From 667698614ff6fb854bccb37e0c7fc904bd439d28 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Aug 2023 12:57:49 -0700 Subject: [PATCH 01/45] x509: add utils to find certs by thumbprint --- src/shared/Core/X509Utils.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/shared/Core/X509Utils.cs diff --git a/src/shared/Core/X509Utils.cs b/src/shared/Core/X509Utils.cs new file mode 100644 index 000000000..e1558d337 --- /dev/null +++ b/src/shared/Core/X509Utils.cs @@ -0,0 +1,23 @@ +using System.Security.Cryptography.X509Certificates; + +namespace GitCredentialManager; + +public static class X509Utils +{ + public static X509Certificate2 GetCertificateByThumbprint(string thumbprint) + { + foreach (var location in new[]{StoreLocation.CurrentUser, StoreLocation.LocalMachine}) + { + using var store = new X509Store(StoreName.My, location); + store.Open(OpenFlags.ReadOnly); + + X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (certs.Count > 0) + { + return certs[0]; + } + } + + return null; + } +} From b62704458ae74ad1ca2367ce9c9f8cd160b4155f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 3 Aug 2023 16:29:34 -0700 Subject: [PATCH 02/45] msauth: rename GetTokenAsync to GetTokenForUserAsync Rename the lone GetToken method to clarify that this is for user principals (regular user identities). This is in preparation for adding extra principal types including service principals, and managed identities. Also add some XML doc comments to the method. --- .../Authentication/MicrosoftAuthenticationTests.cs | 4 ++-- .../Core/Authentication/MicrosoftAuthentication.cs | 14 ++++++++++++-- .../AzureReposHostProviderTests.cs | 14 +++++++------- .../Microsoft.AzureRepos/AzureReposHostProvider.cs | 4 ++-- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs index ef0f50a86..682fad12d 100644 --- a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs +++ b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs @@ -8,7 +8,7 @@ namespace GitCredentialManager.Tests.Authentication public class MicrosoftAuthenticationTests { [Fact] - public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenAsync_NoInteraction_ThrowsException() + public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException() { const string authority = "https://login.microsoftonline.com/common"; const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3"; @@ -24,7 +24,7 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA var msAuth = new MicrosoftAuthentication(context); await Assert.ThrowsAsync( - () => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName, false)); + () => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false)); } } } diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 06bd7330d..b57267d66 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -23,7 +23,17 @@ namespace GitCredentialManager.Authentication { public interface IMicrosoftAuthentication { - Task GetTokenAsync(string authority, string clientId, Uri redirectUri, + /// + /// Acquire an access token for a user principal. + /// + /// Azure authority. + /// Client ID. + /// Redirect URI for the client. + /// Set of scopes to request. + /// Optional user name for an existing account. + /// Use MSA-Passthrough behavior when authenticating. + /// Authentication result. + Task GetTokenForUserAsync(string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt = false); } @@ -59,7 +69,7 @@ public MicrosoftAuthentication(ICommandContext context) #region IMicrosoftAuthentication - public async Task GetTokenAsync( + public async Task GetTokenForUserAsync( string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt) { // Check if we can and should use OS broker authentication diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index d7fc916e1..c607f408d 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -170,7 +170,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -219,7 +219,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, urlAccount, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -268,7 +268,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -315,7 +315,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ var azDevOpsMock = new Mock(MockBehavior.Strict); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -363,7 +363,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_ var azDevOpsMock = new Mock(MockBehavior.Strict); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, account, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, account, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -413,7 +413,7 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthorit azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); @@ -462,7 +462,7 @@ public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_Ge .ReturnsAsync(personalAccessToken); var msAuthMock = new Mock(MockBehavior.Strict); - msAuthMock.Setup(x => x.GetTokenAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) + msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true)) .ReturnsAsync(authResult); var authorityCacheMock = new Mock(MockBehavior.Strict); diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index e696e504d..84d9e7bcf 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -197,7 +197,7 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments // Get an AAD access token for the Azure DevOps SPS _context.Trace.WriteLine("Getting Azure AD access token..."); - IMicrosoftAuthenticationResult result = await _msAuth.GetTokenAsync( + IMicrosoftAuthenticationResult result = await _msAuth.GetTokenForUserAsync( authAuthority, GetClientId(), GetRedirectUri(), @@ -289,7 +289,7 @@ private async Task GetAzureAccessTokenAsync(Inpu // Get an AAD access token for the Azure DevOps SPS _context.Trace.WriteLine("Getting Azure AD access token..."); - IMicrosoftAuthenticationResult result = await _msAuth.GetTokenAsync( + IMicrosoftAuthenticationResult result = await _msAuth.GetTokenForUserAsync( authAuthority, GetClientId(), GetRedirectUri(), From 89b099e17bd15299947406f170ab7271ff19955b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 8 Aug 2023 09:08:13 -0700 Subject: [PATCH 03/45] msauth: abstract token cache init helpers Refactor the token cache helper methods to allow us to re-use the existing cache registration logic with a different ITokenCache and StorageCreationProperties. This will be useful when we later introduce a confidential client application (for service principals) that needs a different cache location, and uses the AppTokenCache, rather than the User one. --- .../Authentication/MicrosoftAuthentication.cs | 34 ++++++++++++------- .../MicrosoftAuthenticationDiagnostic.cs | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index b57267d66..5e3ea0a92 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -422,8 +422,8 @@ internal MicrosoftAuthenticationFlowType GetFlowType() IPublicClientApplication app = appBuilder.Build(); - // Register the application token cache - await RegisterTokenCacheAsync(app, Context.Trace2); + // Register the user token cache + await RegisterTokenCacheAsync(app.UserTokenCache, CreateUserTokenCacheProps, Context.Trace2); return app; } @@ -432,10 +432,11 @@ internal MicrosoftAuthenticationFlowType GetFlowType() #region Helpers - private async Task RegisterTokenCacheAsync(IPublicClientApplication app, ITrace2 trace2) + private delegate StorageCreationProperties StoragePropertiesBuilder(bool useLinuxFallback); + + private async Task RegisterTokenCacheAsync(ITokenCache cache, StoragePropertiesBuilder propsBuilder, ITrace2 trace2) { - Context.Trace.WriteLine( - "Configuring Microsoft Authentication token cache to instance shared with Microsoft developer tools..."); + Context.Trace.WriteLine("Configuring MSAL token cache..."); if (!PlatformUtils.IsWindows() && !PlatformUtils.IsPosix()) { @@ -445,11 +446,11 @@ private async Task RegisterTokenCacheAsync(IPublicClientApplication app, ITrace2 } // We use the MSAL extension library to provide us consistent cache file access semantics (synchronisation, etc) - // as other Microsoft developer tools such as the Azure PowerShell CLI. + // as other GCM processes, and other Microsoft developer tools such as the Azure PowerShell CLI. MsalCacheHelper helper = null; try { - var storageProps = CreateTokenCacheProps(useLinuxFallback: false); + StorageCreationProperties storageProps = propsBuilder(useLinuxFallback: false); helper = await MsalCacheHelper.CreateAsync(storageProps); // Test that cache access is working correctly @@ -477,24 +478,31 @@ private async Task RegisterTokenCacheAsync(IPublicClientApplication app, ITrace2 // On Linux the SecretService/keyring might not be available so we must fall-back to a plaintext file. Context.Streams.Error.WriteLine("warning: using plain-text fallback token cache"); Context.Trace.WriteLine("Using fall-back plaintext token cache on Linux."); - var storageProps = CreateTokenCacheProps(useLinuxFallback: true); + StorageCreationProperties storageProps = propsBuilder(useLinuxFallback: true); helper = await MsalCacheHelper.CreateAsync(storageProps); } } if (helper is null) { - Context.Streams.Error.WriteLine("error: failed to set up Microsoft Authentication token cache!"); - Context.Trace.WriteLine("Failed to integrate with shared token cache!"); + Context.Streams.Error.WriteLine("error: failed to set up token cache!"); + Context.Trace.WriteLine("Failed to integrate with token cache!"); } else { - helper.RegisterCache(app.UserTokenCache); - Context.Trace.WriteLine("Microsoft developer tools token cache configured."); + helper.RegisterCache(cache); + Context.Trace.WriteLine("Token cache configured."); } } - internal StorageCreationProperties CreateTokenCacheProps(bool useLinuxFallback) + /// + /// Create the properties for the user token cache. This is used by public client applications only. + /// This cache is shared between GCM processes, and also other Microsoft developer tools such as the Azure + /// PowerShell CLI. + /// + /// + /// + internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallback) { const string cacheFileName = "msal.cache"; string cacheDirectory; diff --git a/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs b/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs index 05ed9200c..e4dba0822 100644 --- a/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs +++ b/src/shared/Core/Diagnostics/MicrosoftAuthenticationDiagnostic.cs @@ -20,7 +20,7 @@ protected override async Task RunInternalAsync(StringBuilder log, IList Date: Mon, 7 Aug 2023 13:08:41 -0700 Subject: [PATCH 04/45] msauth: add support for service principal auth Add support for acquiring a token for a service principal. Either a client secret or certificate can be used to authenticate (the latter being preferred). --- .../Authentication/MicrosoftAuthentication.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 5e3ea0a92..f3851dbc6 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using GitCredentialManager.Interop.Windows.Native; using Microsoft.Identity.Client; @@ -35,6 +36,43 @@ public interface IMicrosoftAuthentication /// Authentication result. Task GetTokenForUserAsync(string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt = false); + + /// + /// Acquire an access token for the given service principal with the specified scopes. + /// + /// Service principal identity. + /// Scopes to request. + /// Authentication result. + Task GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes); + } + + public class ServicePrincipalIdentity + { + /// + /// Client ID of the service principal. + /// + public string Id { get; set; } + + /// + /// Tenant ID of the service principal. + /// + public string TenantId { get; set; } + + /// + /// Certificate used to authenticate the service principal. + /// + /// + /// If both and are set, the certificate will be used. + /// + public X509Certificate2 Certificate { get; set; } + + /// + /// Secret used to authenticate the service principal. + /// + /// + /// If both and are set, the certificate will be used. + /// + public string ClientSecret { get; set; } } public interface IMicrosoftAuthenticationResult @@ -210,6 +248,23 @@ public MicrosoftAuthentication(ICommandContext context) } } + public async Task GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes) + { + IConfidentialClientApplication app = CreateConfidentialClientApplication(sp); + + try + { + AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); + return new MsalResult(result); + } + catch (Exception ex) + { + Context.Trace.WriteLine($"Failed to acquire token for service principal '{sp.TenantId}/{sp.TenantId}'."); + Context.Trace.WriteException(ex); + throw; + } + } + private async Task UseDefaultAccountAsync(string userName) { ThrowIfUserInteractionDisabled(); @@ -428,6 +483,35 @@ internal MicrosoftAuthenticationFlowType GetFlowType() return app; } + private IConfidentialClientApplication CreateConfidentialClientApplication(ServicePrincipalIdentity sp) + { + var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); + + Context.Trace.WriteLine($"Creating confidential client application for {sp.TenantId}/{sp.Id}..."); + var appBuilder = ConfidentialClientApplicationBuilder.Create(sp.Id) + .WithTenantId(sp.TenantId) + .WithHttpClientFactory(httpFactoryAdaptor); + + if (sp.Certificate is not null) + { + Context.Trace.WriteLineSecrets("Using certificate with thumbprint: '{0}'", new object[] { sp.Certificate.Thumbprint }); + appBuilder = appBuilder.WithCertificate(sp.Certificate); + } + else if (!string.IsNullOrWhiteSpace(sp.ClientSecret)) + { + Context.Trace.WriteLineSecrets("Using client secret: '{0}'", new object[] { sp.ClientSecret }); + appBuilder = appBuilder.WithClientSecret(sp.ClientSecret); + } + else + { + throw new InvalidOperationException("Service principal identity does not contain a certificate or client secret."); + } + + IConfidentialClientApplication app = appBuilder.Build(); + + return app; + } + #endregion #region Helpers From bfa87dba093bbb8a2ffc28288d0b3c3b10fae1c9 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Aug 2023 13:01:49 -0700 Subject: [PATCH 05/45] msauth: add support for managed identity Add support for obtaining an access token using either the system-assigned and a user-assigned managed identity. --- .../MicrosoftAuthenticationTests.cs | 45 ++++++++- .../Authentication/MicrosoftAuthentication.cs | 91 ++++++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs index 682fad12d..0e1a70659 100644 --- a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs +++ b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs @@ -1,6 +1,8 @@ using System; +using System.Threading.Tasks; using GitCredentialManager.Authentication; using GitCredentialManager.Tests.Objects; +using Microsoft.Identity.Client.AppConfig; using Xunit; namespace GitCredentialManager.Tests.Authentication @@ -8,7 +10,7 @@ namespace GitCredentialManager.Tests.Authentication public class MicrosoftAuthenticationTests { [Fact] - public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException() + public async Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException() { const string authority = "https://login.microsoftonline.com/common"; const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3"; @@ -26,5 +28,46 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUser await Assert.ThrowsAsync( () => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false)); } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("system")] + [InlineData("SYSTEM")] + [InlineData("sYsTeM")] + [InlineData("00000000-0000-0000-0000-000000000000")] + [InlineData("id://00000000-0000-0000-0000-000000000000")] + [InlineData("ID://00000000-0000-0000-0000-000000000000")] + [InlineData("Id://00000000-0000-0000-0000-000000000000")] + public void MicrosoftAuthentication_GetManagedIdentity_ValidSystemId_ReturnsSystemId(string str) + { + ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str); + Assert.Equal(ManagedIdentityId.SystemAssigned, actual); + } + + [Theory] + [InlineData("8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("id://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("ID://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("Id://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("resource://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("RESOURCE://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("rEsOuRcE://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("resource://00000000-0000-0000-0000-000000000000")] + public void MicrosoftAuthentication_GetManagedIdentity_ValidUserIdByClientId_ReturnsUserId(string str) + { + ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str); + Assert.NotNull(actual); + Assert.NotEqual(ManagedIdentityId.SystemAssigned, actual); + } + + [Theory] + [InlineData("unknown://8B49DCA0-1298-4A0D-AD6D-934E40230839")] + [InlineData("this is a string")] + public void MicrosoftAuthentication_GetManagedIdentity_Invalid_ThrowsArgumentException(string str) + { + Assert.Throws(() => MicrosoftAuthentication.GetManagedIdentity(str)); + } } } diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index f3851dbc6..3b22bb7a3 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -13,6 +13,7 @@ using GitCredentialManager.UI; using GitCredentialManager.UI.ViewModels; using GitCredentialManager.UI.Views; +using Microsoft.Identity.Client.AppConfig; #if NETFRAMEWORK using System.Drawing; @@ -44,6 +45,25 @@ public interface IMicrosoftAuthentication /// Scopes to request. /// Authentication result. Task GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes); + + /// + /// Acquire a token using the managed identity in the current environment. + /// + /// Managed identity to use. + /// Resource to obtain an access token for. + /// Authentication result including access token. + /// + /// There are several formats for the parameter: + /// + /// - "system" - Use the system-assigned managed identity. + /// + /// - "{guid}" - Use the user-assigned managed identity with client ID {guid}. + /// + /// - "id://{guid}" - Use the user-assigned managed identity with client ID {guid}. + /// + /// - "resource://{guid}" - Use the user-assigned managed identity with resource ID {guid}. + /// + Task GetTokenForManagedIdentityAsync(string managedIdentity, string resource); } public class ServicePrincipalIdentity @@ -265,6 +285,31 @@ public async Task GetTokenForServicePrincipalAsy } } + public async Task GetTokenForManagedIdentityAsync(string managedIdentity, string resource) + { + var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); + + ManagedIdentityId mid = GetManagedIdentity(managedIdentity); + + IManagedIdentityApplication app = ManagedIdentityApplicationBuilder.Create(mid) + .WithHttpClientFactory(httpFactoryAdaptor) + .Build(); + + try + { + AuthenticationResult result = await app.AcquireTokenForManagedIdentity(resource).ExecuteAsync(); + return new MsalResult(result); + } + catch (Exception ex) + { + Context.Trace.WriteLine(mid == ManagedIdentityId.SystemAssigned + ? "Failed to acquire token for system managed identity." + : $"Failed to acquire token for user managed identity '{managedIdentity:D}'."); + Context.Trace.WriteException(ex); + throw; + } + } + private async Task UseDefaultAccountAsync(string userName) { ThrowIfUserInteractionDisabled(); @@ -624,6 +669,50 @@ internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallba return builder.Build(); } + internal static ManagedIdentityId GetManagedIdentity(string str) + { + // An empty string or "system" means system-assigned managed identity + if (string.IsNullOrWhiteSpace(str) || str.Equals("system", StringComparison.OrdinalIgnoreCase)) + { + return ManagedIdentityId.SystemAssigned; + } + + // + // A GUID-looking value means a user-assigned managed identity specified by the client ID. + // If the "{value}" is the empty GUID then we use the system-assigned MI. + // + if (Guid.TryParse(str, out Guid guid)) + { + return guid == Guid.Empty + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(str); + } + + // + // A value of the form "id://{value}" means a user-assigned managed identity specified by the client ID. + // If the "{value}" is the empty GUID then we use the system-assigned MI. + // + // If the value is "resource://{value}" then it is a user-assigned managed identity specified + // by the resource ID. + // + if (Uri.TryCreate(str, UriKind.Absolute, out Uri uri)) + { + if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "id")) + { + return Guid.TryParse(uri.Host, out Guid g) && g == Guid.Empty + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(uri.Host); + } + + if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "resource")) + { + return ManagedIdentityId.WithUserAssignedResourceId(uri.Host); + } + } + + throw new ArgumentException("Invalid managed identity value.", nameof(str)); + } + private static EmbeddedWebViewOptions GetEmbeddedWebViewOptions() { return new EmbeddedWebViewOptions @@ -774,7 +863,7 @@ public MsalResult(AuthenticationResult msalResult) } public string AccessToken => _msalResult.AccessToken; - public string AccountUpn => _msalResult.Account.Username; + public string AccountUpn => _msalResult.Account?.Username; } #if NETFRAMEWORK From f00c859fbadc6543452d59d4b2bd3b56d5f7d2f5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 8 Aug 2023 09:27:29 -0700 Subject: [PATCH 06/45] msauth: add MSAL app token cache support for CCAs Add app token cache support for confidential client applications (service principals). This is a different cache than the one for user tokens that is used by public client applications (for normal users). We do not know of any other app token cache that we can share with currently, so we just use our own in the GCM data directory. --- .../Authentication/MicrosoftAuthentication.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 3b22bb7a3..0dd0fefa4 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -270,7 +270,7 @@ public MicrosoftAuthentication(ICommandContext context) public async Task GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes) { - IConfidentialClientApplication app = CreateConfidentialClientApplication(sp); + IConfidentialClientApplication app = await CreateConfidentialClientApplicationAsync(sp); try { @@ -528,7 +528,7 @@ internal MicrosoftAuthenticationFlowType GetFlowType() return app; } - private IConfidentialClientApplication CreateConfidentialClientApplication(ServicePrincipalIdentity sp) + private async Task CreateConfidentialClientApplicationAsync(ServicePrincipalIdentity sp) { var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); @@ -554,6 +554,8 @@ private IConfidentialClientApplication CreateConfidentialClientApplication(Servi IConfidentialClientApplication app = appBuilder.Build(); + await RegisterTokenCacheAsync(app.AppTokenCache, CreateAppTokenCacheProps, Context.Trace2); + return app; } @@ -713,6 +715,38 @@ internal static ManagedIdentityId GetManagedIdentity(string str) throw new ArgumentException("Invalid managed identity value.", nameof(str)); } + /// + /// Create the properties for the application token cache. This is used by confidential client applications only + /// and is not shared between applications other than GCM. + /// + internal StorageCreationProperties CreateAppTokenCacheProps(bool useLinuxFallback) + { + const string cacheFileName = "app.cache"; + + // The confidential client MSAL cache is located at "%UserProfile%\.gcm\msal\app.cache" on Windows + // and at "~/.gcm/msal/app.cache" on UNIX. + string cacheDirectory = Path.Combine(Context.FileSystem.UserDataDirectoryPath, "msal"); + + // The keychain is used on macOS with the following service & account names + var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory) + .WithMacKeyChain("GitCredentialManager.MSAL", "AppCache"); + + if (useLinuxFallback) + { + builder.WithLinuxUnprotectedFile(); + } + else + { + // The SecretService/keyring is used on Linux with the following collection name and attributes + builder.WithLinuxKeyring(cacheFileName, + "default", "AppCache", + new KeyValuePair("MsalClientID", "GitCredentialManager.MSAL"), + new KeyValuePair("GitCredentialManager.MSAL", "1.0.0.0")); + } + + return builder.Build(); + } + private static EmbeddedWebViewOptions GetEmbeddedWebViewOptions() { return new EmbeddedWebViewOptions From aafbda4a1c1680a0a93d73e5f2f44470d698a6b8 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Aug 2023 13:09:35 -0700 Subject: [PATCH 07/45] azrepos: support service principals and managed IDs Allow a service principal or managed identity to be used to authenticate against Azure Repos. Required information for service principals is specified in Git config or environment variables, as is the ID for a managed identity. --- docs/configuration.md | 99 ++++++++++++++ docs/environment.md | 127 +++++++++++++++++- .../AzureDevOpsConstants.cs | 11 +- .../AzureReposHostProvider.cs | 121 ++++++++++++++++- 4 files changed, 353 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 268e35c40..88a23c103 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -793,6 +793,95 @@ git config --global credential.azreposCredentialType oauth --- +### credential.azreposManagedIdentity + +Use a [Managed Identity][managed-identity] to authenticate with Azure Repos. + +The value `system` will tell GCM to use the system-assigned Managed Identity. + +To specify a user-assigned Managed Identity, use the format `id://{clientId}` +where `{clientId}` is the client ID of the Managed Identity. Alternatively any +GUID-like value will also be interpreted as a user-assigned Managed Identity +client ID. + +To specify a Managed Identity associated with an Azure resource, you can use the +format `resource://{resourceId}` where `{resourceId}` is the ID of the resource. + +For more information about managed identities, see the Azure DevOps +[documentation][azrepos-sp-mid]. + +Value|Description +-|- +`system`|System-Assigned Managed Identity +`[guid]`|User-Assigned Managed Identity with the specified client ID +`id://[guid]`|User-Assigned Managed Identity with the specified client ID +`resource://[guid]`|User-Assigned Managed Identity for the associated resource + +```shell +git config --global credential.azreposManagedIdentity "id://11111111-1111-1111-1111-111111111111" +``` + +**Also see: [GCM_AZREPOS_MANAGEDIDENTITY][gcm-azrepos-credentialmanagedidentity]** + +--- + +### credential.azreposServicePrincipal + +Specify the client and tenant IDs of a [service principal][service-principal] +to use when performing Microsoft authentication for Azure Repos. + +The value of this setting should be in the format: `{tenantId}/{clientId}`. + +You must also set at least one authentication mechanism if you set this value: + +- [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret] +- [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint] + +For more information about service principals, see the Azure DevOps +[documentation][azrepos-sp-mid]. + +#### Example + +```shell +git config --global credential.azreposServicePrincipal "11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222" +``` + +**Also see: [GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-service-principal]** + +--- + +### credential.azreposServicePrincipalSecret + +Specifies the client secret for the [service principal][service-principal] when +performing Microsoft authentication for Azure Repos with +[credential.azreposServicePrincipalSecret][credential-azrepos-sp] set. + +#### Example + +```shell +git config --global credential.azreposServicePrincipalSecret "da39a3ee5e6b4b0d3255bfef95601890afd80709" +``` + +**Also see: [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret]** + +--- + +### credential.azreposServicePrincipalCertificateThumbprint + +Specifies the thumbprint of a certificate to use when authenticating as a +[service principal][service-principal] for Azure Repos when +[GCM_AZREPOS_SERVICE_PRINCIPAL][credential-azrepos-sp] is set. + +#### Example + +```shell +git config --global credential.azreposServicePrincipalCertificateThumbprint "9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc" +``` + +**Also see: [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint]** + +--- + ### trace2.normalTarget Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format @@ -878,6 +967,7 @@ Defaults to disabled. [gcm-authority]: environment.md#GCM_AUTHORITY-deprecated [gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT [gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE +[gcm-azrepos-credentialmanagedidentity]: environment.md#GCM_AZREPOS_MANAGEDIDENTITY [gcm-bitbucket-always-refresh-credentials]: environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS [gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES [gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS @@ -905,6 +995,7 @@ Defaults to disabled. [http-proxy]: netconfig.md#http-proxy [autodetect]: autodetect.md [libsecret]: https://wiki.gnome.org/Projects/Libsecret +[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview [provider-migrate]: migration.md#gcm_authority [cache-options]: https://git-scm.com/docs/git-credential-cache#_options [pass]: https://www.passwordstore.org/ @@ -915,3 +1006,11 @@ Defaults to disabled. [trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target [trace2-performance-env]: environment.md#GIT_TRACE2_PERF [wam]: windows-broker.md +[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals +[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity +[credential-azrepos-sp]: #credentialazreposserviceprincipal +[credential-azrepos-sp-secret]: #credentialazreposserviceprincipalsecret +[credential-azrepos-sp-cert-thumbprint]: #credentialazreposserviceprincipalcertificatethumbprint +[gcm-azrepos-service-principal]: environment.md#GCM_AZREPOS_SERVICE_PRINCIPAL +[gcm-azrepos-sp-secret]: environment.md#GCM_AZREPOS_SP_SECRET +[gcm-azrepos-sp-cert-thumbprint]: environment.md#GCM_AZREPOS_SP_CERT_THUMBPRINT diff --git a/docs/environment.md b/docs/environment.md index f3d8a618e..666604422 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -894,6 +894,121 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth" --- +### GCM_AZREPOS_MANAGEDIDENTITY + +Use a [Managed Identity][managed-identity] to authenticate with Azure Repos. + +The value `system` will tell GCM to use the system-assigned Managed Identity. + +To specify a user-assigned Managed Identity, use the format `id://{clientId}` +where `{clientId}` is the client ID of the Managed Identity. Alternatively any +GUID-like value will also be interpreted as a user-assigned Managed Identity +client ID. + +To specify a Managed Identity associated with an Azure resource, you can use the +format `resource://{resourceId}` where `{resourceId}` is the ID of the resource. + +For more information about managed identities, see the Azure DevOps +[documentation][azrepos-sp-mid]. + +Value|Description +-|- +`system`|System-Assigned Managed Identity +`[guid]`|User-Assigned Managed Identity with the specified client ID +`id://[guid]`|User-Assigned Managed Identity with the specified client ID +`resource://[guid]`|User-Assigned Managed Identity for the associated resource + +#### Windows + +```batch +SET GCM_AZREPOS_MANAGEDIDENTITY="id://11111111-1111-1111-1111-111111111111" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_MANAGEDIDENTITY="id://11111111-1111-1111-1111-111111111111" +``` + +**Also see: [credential.azreposManagedIdentity][credential-azrepos-managedidentity]** + +--- + +### GCM_AZREPOS_SERVICE_PRINCIPAL + +Specify the client and tenant IDs of a [service principal][service-principal] +to use when performing Microsoft authentication for Azure Repos. + +The value of this setting should be in the format: `{tenantId}/{clientId}`. + +You must also set at least one authentication mechanism if you set this value: + +- [GCM_AZREPOS_SP_SECRET][gcm-azrepos-sp-secret] +- [GCM_AZREPOS_SP_CERT_THUMBPRINT][gcm-azrepos-sp-cert-thumbprint] + +For more information about service principals, see the Azure DevOps +[documentation][azrepos-sp-mid]. + +#### Windows + +```batch +SET GCM_AZREPOS_SERVICE_PRINCIPAL="11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_SERVICE_PRINCIPAL="11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222" +``` + +**Also see: [credential.azreposServicePrincipal][credential-azrepos-sp]** + +--- + +### GCM_AZREPOS_SP_SECRET + +Specifies the client secret for the [service principal][service-principal] when +performing Microsoft authentication for Azure Repos with +[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] set. + +#### Windows + +```batch +SET GCM_AZREPOS_SP_SECRET="da39a3ee5e6b4b0d3255bfef95601890afd80709" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_SP_SECRET="da39a3ee5e6b4b0d3255bfef95601890afd80709" +``` + +**Also see: [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]** + +--- + +### GCM_AZREPOS_SP_CERT_THUMBPRINT + +Specifies the thumbprint of a certificate to use when authenticating as a +[service principal][service-principal] for Azure Repos when +[GCM_AZREPOS_SERVICE_PRINCIPAL][gcm-azrepos-sp] is set. + +#### Windows + +```batch +SET GCM_AZREPOS_SP_CERT_THUMBPRINT="9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_SP_CERT_THUMBPRINT="9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc" +``` + +**Also see: [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]** + +--- + ### GIT_TRACE2 Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format @@ -985,7 +1100,8 @@ Defaults to disabled. [credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth [credential-authority]: configuration.md#credentialauthority-deprecated [credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout -[credential-azrepos-credential-type]: configuration.md#azreposcredentialtype +[credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype +[credential-azrepos-managedidentity]: configuration.md#credentialazreposmanagedidentity [credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes [credential-cacheoptions]: configuration.md#credentialcacheoptions [credential-credentialstore]: configuration.md#credentialcredentialstore @@ -1022,6 +1138,7 @@ Defaults to disabled. [github-emu]: https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-enterprise-managed-users-for-iam/about-enterprise-managed-users [network-http-proxy]: netconfig.md#http-proxy [libsecret]: https://wiki.gnome.org/Projects/Libsecret +[managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview [migration-guide]: migration.md#gcm_authority [passwordstore]: https://www.passwordstore.org/ [trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target @@ -1031,3 +1148,11 @@ Defaults to disabled. [trace2-performance-docs]: https://git-scm.com/docs/api-trace2#_the_performance_format_target [trace2-performance-config]: configuration.md#trace2perfTarget [windows-broker]: windows-broker.md +[service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals +[azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity +[gcm-azrepos-sp]: #gcm_azrepos_service_principal +[gcm-azrepos-sp-secret]: #gcm_azrepos_sp_secret +[gcm-azrepos-sp-cert-thumbprint]: #gcm_azrepos_sp_cert_thumbprint +[credential-azrepos-sp]: configuration.md#credentialazreposserviceprincipal +[credential-azrepos-sp-secret]: configuration.md#credentialazreposserviceprincipalsecret +[credential-azrepos-sp-cert-thumbprint]: configuration.md#credentialazreposserviceprincipalcertificatethumbprint diff --git a/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs b/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs index 2bd239305..c46f08c33 100644 --- a/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs +++ b/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs @@ -8,7 +8,8 @@ internal static class AzureDevOpsConstants public const string AadAuthorityBaseUrl = "https://login.microsoftonline.com"; // Azure DevOps's app ID + default scopes - public static readonly string[] AzureDevOpsDefaultScopes = {"499b84ac-1321-427f-aa17-267ca6975798/.default"}; + public const string AzureDevOpsResourceId = "499b84ac-1321-427f-aa17-267ca6975798"; + public static readonly string[] AzureDevOpsDefaultScopes = {$"{AzureDevOpsResourceId}/.default"}; // Visual Studio's client ID // We share this to be able to consume existing access tokens from the VS caches @@ -40,6 +41,10 @@ public static class EnvironmentVariables public const string DevAadRedirectUri = "GCM_DEV_AZREPOS_REDIRECTURI"; public const string DevAadAuthorityBaseUri = "GCM_DEV_AZREPOS_AUTHORITYBASEURI"; public const string CredentialType = "GCM_AZREPOS_CREDENTIALTYPE"; + public const string ServicePrincipalId = "GCM_AZREPOS_SERVICE_PRINCIPAL"; + public const string ServicePrincipalSecret = "GCM_AZREPOS_SP_SECRET"; + public const string ServicePrincipalCertificateThumbprint = "GCM_AZREPOS_SP_CERT_THUMBPRINT"; + public const string ManagedIdentity = "GCM_AZREPOS_MANAGEDIDENTITY"; } public static class GitConfiguration @@ -51,6 +56,10 @@ public static class Credential public const string DevAadAuthorityBaseUri = "azreposDevAuthorityBaseUri"; public const string CredentialType = "azreposCredentialType"; public const string AzureAuthority = "azureAuthority"; + public const string ServicePrincipal = "azreposServicePrincipal"; + public const string ServicePrincipalSecret = "azreposServicePrincipalSecret"; + public const string ServicePrincipalCertificateThumbprint = "azreposServicePrincipalCertificateThumbprint"; + public const string ManagedIdentity = "azreposManagedIdentity"; } } } diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 84d9e7bcf..941b2bd53 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.Linq; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Threading.Tasks; using GitCredentialManager; @@ -75,6 +76,20 @@ public bool IsSupported(HttpResponseMessage response) public async Task GetCredentialAsync(InputArguments input) { + if (UseManagedIdentity(out string mid)) + { + _context.Trace.WriteLine($"Getting Azure Access Token for managed identity {mid}..."); + var azureResult = await _msAuth.GetTokenForManagedIdentityAsync(mid, AzureDevOpsConstants.AzureDevOpsResourceId); + return new GitCredential(mid, azureResult.AccessToken); + } + + if (UseServicePrincipal(out ServicePrincipalIdentity sp)) + { + _context.Trace.WriteLine($"Getting Azure Access Token for service principal {sp.TenantId}/{sp.Id}..."); + var azureResult = await _msAuth.GetTokenForServicePrincipalAsync(sp, AzureDevOpsConstants.AzureDevOpsDefaultScopes); + return new GitCredential(sp.Id, azureResult.AccessToken); + } + if (UsePersonalAccessTokens()) { Uri remoteUri = input.GetRemoteUri(); @@ -113,7 +128,15 @@ public Task StoreCredentialAsync(InputArguments input) { Uri remoteUri = input.GetRemoteUri(); - if (UsePersonalAccessTokens()) + if (UseManagedIdentity(out _)) + { + _context.Trace.WriteLine("Nothing to store for managed identity authentication."); + } + else if (UseServicePrincipal(out _)) + { + _context.Trace.WriteLine("Nothing to store for service principal authentication."); + } + else if (UsePersonalAccessTokens()) { string service = GetServiceName(remoteUri); @@ -140,13 +163,22 @@ public Task EraseCredentialAsync(InputArguments input) { Uri remoteUri = input.GetRemoteUri(); - if (UsePersonalAccessTokens()) + if (UseManagedIdentity(out _)) + { + _context.Trace.WriteLine("Nothing to erase for managed identity authentication."); + } + else if (UseServicePrincipal(out _)) + { + _context.Trace.WriteLine("Nothing to erase for service principal authentication."); + } + else if (UsePersonalAccessTokens()) { string service = GetServiceName(remoteUri); string account = GetAccountNameForCredentialQuery(input); // Try to locate an existing credential - _context.Trace.WriteLine($"Erasing stored credential in store with service={service} account={account}..."); + _context.Trace.WriteLine( + $"Erasing stored credential in store with service={service} account={account}..."); if (_context.CredentialStore.Remove(service, account)) { _context.Trace.WriteLine("Credential was successfully erased."); @@ -461,6 +493,89 @@ private bool UsePersonalAccessTokens() return defaultValue; } + private bool UseServicePrincipal(out ServicePrincipalIdentity sp) + { + if (!_context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalId, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipal, + out string spStr) || string.IsNullOrWhiteSpace(spStr)) + { + sp = null; + return false; + } + + string[] split = spStr.Split(new[] { '/' }, count: 2); + + if (split.Length < 1 || string.IsNullOrWhiteSpace(split[0])) + { + _context.Streams.Error.WriteLine("error: unable to use configured service principal - missing tenant ID in configuration"); + sp = null; + return false; + } + + if (split.Length < 2 || string.IsNullOrWhiteSpace(split[1])) + { + _context.Streams.Error.WriteLine("error: unable to use configured service principal - missing client ID in configuration"); + sp = null; + return false; + } + + string tenantId = split[0]; + string clientId = split[1]; + + sp = new ServicePrincipalIdentity + { + Id = clientId, + TenantId = tenantId, + }; + + bool hasClientSecret = _context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalSecret, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalSecret, + out string clientSecret); + + bool hasCertThumbprint = _context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalCertificateThumbprint, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalCertificateThumbprint, + out string certThumbprint); + + if (hasCertThumbprint && hasClientSecret) + { + _context.Streams.Error.WriteLine("warning: both service principal client secret and certificate thumbprint are configured - using certificate"); + } + + if (hasCertThumbprint) + { + X509Certificate2 cert = X509Utils.GetCertificateByThumbprint(certThumbprint); + if (cert is null) + { + _context.Streams.Error.WriteLine($"error: unable to find certificate with thumbprint '{certThumbprint}' for service principal"); + return false; + } + + sp.Certificate = cert; + } + else if (hasClientSecret) + { + sp.ClientSecret = clientSecret; + } + + return true; + } + + private bool UseManagedIdentity(out string mid) + { + return _context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.ManagedIdentity, + KnownGitCfg.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.ManagedIdentity, + out mid) && + !string.IsNullOrWhiteSpace(mid); + } + #endregion #region IConfigurationComponent From eff4ea6fd47ed77c5ce0b733ea17532c5edc5ff0 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 15 Aug 2023 15:16:22 -0700 Subject: [PATCH 08/45] azrepos: add tests of MID and SP get credential Add tests of the `GetCredentialAsync` method on the `AzureReposHostProvider` using managed identity and service principal. --- .../AzureReposHostProviderTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index c607f408d..e65674825 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -511,6 +511,102 @@ public async Task AzureReposProvider_GetCredentialAsync_PatMode_ExistingPat_Retu Assert.Equal(personalAccessToken, credential.Password); } + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_ManagedIdentity_ReturnsManagedIdCredential() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "MANAGED-IDENTITY-TOKEN"; + const string managedIdentity = "MANAGED-IDENTITY"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.ManagedIdentity] = managedIdentity + } + } + }; + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => x.GetTokenForManagedIdentityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + ICredential credential = await provider.GetCredentialAsync(input); + + Assert.NotNull(credential); + Assert.Equal(managedIdentity, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify( + x => x.GetTokenForManagedIdentityAsync(managedIdentity, + AzureDevOpsConstants.AzureDevOpsResourceId), Times.Once); + } + + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_ServicePrincipal_ReturnsSPCredential() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "SP-TOKEN"; + const string tenantId = "78B1822F-107D-40A3-A29C-AB68D8066074"; + const string clientId = "49B4DC1A-58A8-4EEE-A81B-616A40D0BA64"; + const string servicePrincipal = $"{tenantId}/{clientId}"; + const string servicePrincipalSecret = "CLIENT-SECRET"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalId] = servicePrincipal, + [AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalSecret] = servicePrincipalSecret + } + } + }; + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => + x.GetTokenForServicePrincipalAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + ICredential credential = await provider.GetCredentialAsync(input); + + Assert.NotNull(credential); + Assert.Equal(clientId, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify(x => x.GetTokenForServicePrincipalAsync( + It.Is(sp => sp.TenantId == tenantId && sp.Id == clientId), + It.Is(scopes => scopes.Length == 1 && scopes[0] == AzureDevOpsConstants.AzureDevOpsDefaultScopes[0])), + Times.Once); + } + [Fact] public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathSetTrue_DoesNothing() { From 0704b428f448db8b2dff437d0673d3acb513d6bc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 16 Aug 2023 12:08:17 -0700 Subject: [PATCH 09/45] release.yml: use correct target_commitish arg Use the correct name for setting the target of a tag in the release workflow, for creating a draft release. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fc24ea3c..aa94491e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -749,7 +749,7 @@ jobs: ...releaseMetadata, draft: true, tag_name: tagName, - tag_commitish: context.sha, + target_commitish: context.sha, name: `GCM ${version}` }); releaseMetadata.release_id = createdRelease.data.id; From 527c41b2328dd80bfe8db9fdd4f4f91b863ac5ef Mon Sep 17 00:00:00 2001 From: Hanjiang Yu Date: Thu, 17 Aug 2023 20:25:44 +0800 Subject: [PATCH 10/45] Use Avalonia 11.0.3 --- src/shared/Core/Core.csproj | 12 ++++++------ src/shared/Core/UI/AvaloniaUi.cs | 4 ++-- .../GitHub/UI/Controls/SixDigitInput.axaml.cs | 16 +++++----------- .../GitHub/UI/Views/SelectAccountView.axaml | 2 +- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/shared/Core/Core.csproj b/src/shared/Core/Core.csproj index d537e8c83..0185dcb6c 100644 --- a/src/shared/Core/Core.csproj +++ b/src/shared/Core/Core.csproj @@ -14,24 +14,24 @@ - + - + - - - + + + - + diff --git a/src/shared/Core/UI/AvaloniaUi.cs b/src/shared/Core/UI/AvaloniaUi.cs index f71d7dd18..65b681884 100644 --- a/src/shared/Core/UI/AvaloniaUi.cs +++ b/src/shared/Core/UI/AvaloniaUi.cs @@ -124,11 +124,11 @@ private static void SetParentExternal(Window window, IntPtr parentHandle) return; } - IntPtr ourHandle = window.PlatformImpl.Handle.Handle; + IntPtr ourHandle = window.TryGetPlatformHandle()!.Handle; // Get the desktop scaling factor from our window instance so we // can calculate rects correctly for both our window, and the parent window. - double scaling = window.PlatformImpl.DesktopScaling; + double scaling = window.RenderScaling; // Get our window rect var ourRect = new PixelRect( diff --git a/src/shared/GitHub/UI/Controls/SixDigitInput.axaml.cs b/src/shared/GitHub/UI/Controls/SixDigitInput.axaml.cs index 3be8497b1..524bc959e 100644 --- a/src/shared/GitHub/UI/Controls/SixDigitInput.axaml.cs +++ b/src/shared/GitHub/UI/Controls/SixDigitInput.axaml.cs @@ -5,7 +5,6 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Input; -using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using GitCredentialManager; @@ -22,8 +21,6 @@ public partial class SixDigitInput : UserControl, IFocusable (o, v) => o.Text = v, defaultBindingMode: BindingMode.TwoWay); - private PlatformHotkeyConfiguration _keyMap; - private IClipboard _clipboard; private bool _ignoreTextBoxUpdate; private TextBox[] _textBoxes; private string _text; @@ -37,8 +34,6 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - _keyMap = AvaloniaLocator.Current.GetService(); - _clipboard = AvaloniaLocator.Current.GetService(); _textBoxes = new[] { this.FindControl("one"), @@ -89,7 +84,7 @@ public void SetFocus() { // Workaround: https://github.com/git-ecosystem/git-credential-manager/issues/1293 if (!PlatformUtils.IsMacOS()) - KeyboardDevice.Instance.SetFocusedElement(_textBoxes[0], NavigationMethod.Tab, KeyModifiers.None); + _textBoxes[0].Focus(NavigationMethod.Tab, KeyModifiers.None); } private void SetUpTextBox(TextBox textBox) @@ -99,7 +94,7 @@ private void SetUpTextBox(TextBox textBox) void OnPreviewKeyDown(object sender, KeyEventArgs e) { // Handle paste - if (_keyMap.Paste.Any(x => x.Matches(e))) + if (TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration.Paste.Any(x => x.Matches(e)) ?? false) { OnPaste(); e.Handled = true; @@ -166,8 +161,7 @@ void OnPreviewKeyDown(object sender, KeyEventArgs e) private void OnPaste() { - string text = _clipboard.GetTextAsync().GetAwaiter().GetResult(); - Text = text; + Text = TopLevel.GetTopLevel(this)?.Clipboard?.GetTextAsync().GetAwaiter().GetResult(); } private bool MoveNext() => MoveFocus(true); @@ -177,7 +171,7 @@ private void OnPaste() private bool MoveFocus(bool next) { // Get currently focused text box - if (FocusManager.Instance.Current is TextBox textBox) + if (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() is TextBox textBox) { int textBoxIndex = Array.IndexOf(_textBoxes, textBox); if (textBoxIndex > -1) @@ -186,7 +180,7 @@ private bool MoveFocus(bool next) ? Math.Min(_textBoxes.Length - 1, textBoxIndex + 1) : Math.Max(0, textBoxIndex - 1); - KeyboardDevice.Instance.SetFocusedElement(_textBoxes[nextIndex], NavigationMethod.Tab, KeyModifiers.None); + _textBoxes[nextIndex].Focus(NavigationMethod.Tab, KeyModifiers.None); return true; } } diff --git a/src/shared/GitHub/UI/Views/SelectAccountView.axaml b/src/shared/GitHub/UI/Views/SelectAccountView.axaml index 2e497283c..417d58387 100644 --- a/src/shared/GitHub/UI/Views/SelectAccountView.axaml +++ b/src/shared/GitHub/UI/Views/SelectAccountView.axaml @@ -43,7 +43,7 @@ - Date: Sat, 19 Aug 2023 23:09:58 +0800 Subject: [PATCH 11/45] Update to Avalonia 11.0.4 --- src/shared/Core/Core.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/shared/Core/Core.csproj b/src/shared/Core/Core.csproj index 0185dcb6c..644d07e4e 100644 --- a/src/shared/Core/Core.csproj +++ b/src/shared/Core/Core.csproj @@ -14,24 +14,24 @@ - + - + - - - + + + - + From 3fb216dd18af4072e528937fe4dc1525874c76fe Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Fri, 18 Aug 2023 19:37:15 -0600 Subject: [PATCH 12/45] release: update homebrew deployment Update `release-homebrew` workflow to use the `brew bump-cask-pr` command [1]. This aligns with the Homebrew project's official recommendations for creating PRs in the `Homebrew/homebrew-cask` tap and removes GCM's dependency on the `mjcheetham/update-homebrew` task [2]. 1: https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request#submit-a-new-version-of-an-existing-cask 2: https://github.com/mjcheetham/update-homebrew --- .github/workflows/release-homebrew.yaml | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release-homebrew.yaml b/.github/workflows/release-homebrew.yaml index 1b7951c1b..a27735d36 100644 --- a/.github/workflows/release-homebrew.yaml +++ b/.github/workflows/release-homebrew.yaml @@ -5,17 +5,21 @@ on: jobs: release: - runs-on: ubuntu-latest + runs-on: macos-latest environment: release + env: + HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} steps: - - name: Update Homebrew tap - uses: mjcheetham/update-homebrew@v1.4 - with: - token: ${{ secrets.HOMEBREW_TOKEN }} - tap: Homebrew/homebrew-cask - name: git-credential-manager - type: cask - alwaysUsePullRequest: true - releaseAsset: | - gcm-osx-x64-(.*)\.pkg - gcm-osx-arm64-(.*)\.pkg + - name: Open PR against homebrew/homebrew-cask + run: | + # Get latest version + version=$(curl --silent "https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest" | + grep '"tag_name":' | + sed -E 's/.*"v([0-9\.]+).*/\1/') + + # Ensure local Homebrew repository is up to date + cd "$(brew --repository homebrew/cask)" + git pull + + # Open PR to update to latest version + brew bump-cask-pr git-credential-manager --version $version --no-audit --no-browse From b501b8b95cff52dc19678677604a5032096fe0fc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 5 Sep 2023 14:22:11 -0700 Subject: [PATCH 13/45] github: support gist remote URLs for GitHub We have not been consistently detecting or normalising "gist" URLs for dotcom or GHES instances. Gists are backed by a Git repository and can be cloned/pushed-to etc like a normal repository. Credentials are the same as the base site. Update our OAuth, rest API, and dotcom-detection methods that deal with the remote or target URL to correctly support gists URLs. Also add some tests around this. --- .../GitHub.Tests/GitHubHostProviderTests.cs | 5 +++++ src/shared/GitHub.Tests/GitHubRestApiTests.cs | 16 ++++++++++++++-- src/shared/GitHub/GitHubHostProvider.cs | 11 +++++++---- src/shared/GitHub/GitHubOAuth2Client.cs | 5 ++++- src/shared/GitHub/GitHubRestApi.cs | 9 +++++++-- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/shared/GitHub.Tests/GitHubHostProviderTests.cs b/src/shared/GitHub.Tests/GitHubHostProviderTests.cs index aa085b34c..573ab4e08 100644 --- a/src/shared/GitHub.Tests/GitHubHostProviderTests.cs +++ b/src/shared/GitHub.Tests/GitHubHostProviderTests.cs @@ -15,8 +15,11 @@ public class GitHubHostProviderTests [InlineData("https://github.com", true)] [InlineData("https://gitHUB.CoM", true)] [InlineData("https://GITHUB.COM", true)] + [InlineData("https://gist.github.com", true)] [InlineData("https://foogithub.com", false)] [InlineData("https://api.github.com", false)] + [InlineData("https://api.gist.github.com", false)] + [InlineData("https://foogist.github.com", false)] public void GitHubHostProvider_IsGitHubDotCom(string input, bool expected) { Assert.Equal(expected, GitHubHostProvider.IsGitHubDotCom(new Uri(input))); @@ -98,6 +101,8 @@ public void GitHubHostProvider_GetCredentialServiceUrl(string protocol, string h [InlineData("https://GitHub.Com", "none", GitHubConstants.DotComAuthenticationModes)] [InlineData("https://github.com", null, GitHubConstants.DotComAuthenticationModes)] [InlineData("https://GitHub.Com", null, GitHubConstants.DotComAuthenticationModes)] + [InlineData("https://gist.github.com", null, GitHubConstants.DotComAuthenticationModes)] + [InlineData("https://GIST.GITHUB.COM", null, GitHubConstants.DotComAuthenticationModes)] public async Task GitHubHostProvider_GetSupportedAuthenticationModes(string uriString, string gitHubAuthModes, AuthenticationModes expectedModes) { var targetUri = new Uri(uriString); diff --git a/src/shared/GitHub.Tests/GitHubRestApiTests.cs b/src/shared/GitHub.Tests/GitHubRestApiTests.cs index 01f1cb0e8..c0c42e089 100644 --- a/src/shared/GitHub.Tests/GitHubRestApiTests.cs +++ b/src/shared/GitHub.Tests/GitHubRestApiTests.cs @@ -2,8 +2,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; using System.Threading.Tasks; using GitCredentialManager.Tests; using GitCredentialManager.Tests.Objects; @@ -13,6 +11,20 @@ namespace GitHub.Tests { public class GitHubRestApiTests { + [Theory] + [InlineData("https://github.com", "user", "https://api.github.com/user")] + [InlineData("https://github.com", "users/123", "https://api.github.com/users/123")] + [InlineData("https://gItHuB.cOm", "uSeRs/123", "https://api.github.com/uSeRs/123")] + [InlineData("https://gist.github.com", "user", "https://api.github.com/user")] + [InlineData("https://github.example.com", "user", "https://github.example.com/api/v3/user")] + [InlineData("https://raw.github.example.com", "user", "https://github.example.com/api/v3/user")] + [InlineData("https://gist.github.example.com", "user", "https://github.example.com/api/v3/user")] + public void GitHubRestApi_GetApiRequestUri(string targetUrl, string apiUrl, string expected) + { + Uri actualUri = GitHubRestApi.GetApiRequestUri(new Uri(targetUrl), apiUrl); + Assert.Equal(expected, actualUri.ToString()); + } + [Fact] public async Task GitHubRestApi_AcquireTokenAsync_NullUri_ThrowsException() { diff --git a/src/shared/GitHub/GitHubHostProvider.cs b/src/shared/GitHub/GitHubHostProvider.cs index 6e96e3621..918e859a0 100644 --- a/src/shared/GitHub/GitHubHostProvider.cs +++ b/src/shared/GitHub/GitHubHostProvider.cs @@ -487,10 +487,12 @@ public static bool IsGitHubDotCom(Uri targetUri) { EnsureArgument.AbsoluteUri(targetUri, nameof(targetUri)); - return StringComparer.OrdinalIgnoreCase.Equals(targetUri.Host, GitHubConstants.GitHubBaseUrlHost); + // github.com or gist.github.com are both considered dotcom + return StringComparer.OrdinalIgnoreCase.Equals(targetUri.Host, GitHubConstants.GitHubBaseUrlHost) || + StringComparer.OrdinalIgnoreCase.Equals(targetUri.Host, GitHubConstants.GistBaseUrlHost); } - private static Uri NormalizeUri(Uri uri) + internal static Uri NormalizeUri(Uri uri) { if (uri is null) { @@ -500,8 +502,9 @@ private static Uri NormalizeUri(Uri uri) // Special case for gist.github.com which are git backed repositories under the hood. // Credentials for these repositories are the same as the one stored with "github.com". // Same for gist.github[.subdomain].domain.tld. The general form was already checked via IsSupported. - int firstDot = uri.DnsSafeHost.IndexOf("."); - if (firstDot > -1 && uri.DnsSafeHost.Substring(0, firstDot).Equals("gist", StringComparison.OrdinalIgnoreCase)) { + int firstDot = uri.DnsSafeHost.IndexOf(".", StringComparison.Ordinal); + if (firstDot > -1 && uri.DnsSafeHost.Substring(0, firstDot).Equals("gist", StringComparison.OrdinalIgnoreCase)) + { return new Uri("https://" + uri.DnsSafeHost.Substring(firstDot+1)); } diff --git a/src/shared/GitHub/GitHubOAuth2Client.cs b/src/shared/GitHub/GitHubOAuth2Client.cs index 437b4c066..2eb4aae88 100644 --- a/src/shared/GitHub/GitHubOAuth2Client.cs +++ b/src/shared/GitHub/GitHubOAuth2Client.cs @@ -11,8 +11,11 @@ public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri : base(httpClient, CreateEndpoints(baseUri), GetClientId(settings), trace2, GetRedirectUri(settings, baseUri), GetClientSecret(settings)) { } - private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri) + private static OAuth2ServerEndpoints CreateEndpoints(Uri uri) { + // Ensure that the base URI is normalized to support Gist subdomains + Uri baseUri = GitHubHostProvider.NormalizeUri(uri); + Uri authEndpoint = new Uri(baseUri, GitHubConstants.OAuthAuthorizationEndpointRelativeUri); Uri tokenEndpoint = new Uri(baseUri, GitHubConstants.OAuthTokenEndpointRelativeUri); Uri deviceAuthEndpoint = new Uri(baseUri, GitHubConstants.OAuthDeviceEndpointRelativeUri); diff --git a/src/shared/GitHub/GitHubRestApi.cs b/src/shared/GitHub/GitHubRestApi.cs index 783812b8e..5051ed2bb 100644 --- a/src/shared/GitHub/GitHubRestApi.cs +++ b/src/shared/GitHub/GitHubRestApi.cs @@ -203,7 +203,7 @@ private async Task ParseSuccessResponseAsync(Uri targetUri } } - private Uri GetApiRequestUri(Uri targetUri, string apiUrl) + internal /* for testing */ static Uri GetApiRequestUri(Uri targetUri, string apiUrl) { if (GitHubHostProvider.IsGitHubDotCom(targetUri)) { @@ -214,8 +214,13 @@ private Uri GetApiRequestUri(Uri targetUri, string apiUrl) // If we're here, it's GitHub Enterprise via a configured authority var baseUrl = targetUri.GetLeftPart(UriPartial.Authority); + RegexOptions reOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase; + // Check for 'raw.' in the hostname and remove it to get the correct GHE API URL - baseUrl = Regex.Replace(baseUrl, @"^(https?://)raw\.", "$1", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + baseUrl = Regex.Replace(baseUrl, @"^(https?://)raw\.", "$1", reOptions); + + // Likewise check for `gist.` in the hostname and remove it to get the correct GHE API URL + baseUrl = Regex.Replace(baseUrl, @"^(https?://)gist\.", "$1", reOptions); return new Uri(baseUrl + $"/api/v3/{apiUrl}"); } From 37744138158c145c7f14f226ff7aca550dc21996 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 11 Sep 2023 13:07:18 -0700 Subject: [PATCH 14/45] bitbucket: drop WPF-based UI helper for Bitbucket --- Git-Credential-Manager.sln | 13 -- .../Assets/Styles.xaml | 6 - .../Assets/atlassian-logo.png | Bin 13455 -> 0 bytes .../Atlassian.Bitbucket.UI.Windows.csproj | 25 ---- .../Commands/CredentialsCommandImpl.cs | 20 --- .../Controls/TesterWindow.xaml | 35 ----- .../Controls/TesterWindow.xaml.cs | 38 ------ .../Atlassian.Bitbucket.UI.Windows/Program.cs | 45 ------- .../Views/CredentialsView.xaml | 121 ------------------ .../Views/CredentialsView.xaml.cs | 73 ----------- .../Installer.Windows.csproj | 1 - src/windows/Installer.Windows/layout.ps1 | 6 - 12 files changed, 383 deletions(-) delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Assets/Styles.xaml delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Assets/atlassian-logo.png delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Atlassian.Bitbucket.UI.Windows.csproj delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Commands/CredentialsCommandImpl.cs delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml.cs delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml delete mode 100644 src/windows/Atlassian.Bitbucket.UI.Windows/Views/CredentialsView.xaml.cs diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln index 75e1254b7..dfbb59868 100644 --- a/Git-Credential-Manager.sln +++ b/Git-Credential-Manager.sln @@ -43,8 +43,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "linux", "linux", "{8F9D7E67 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Windows", "src\windows\GitHub.UI.Windows\GitHub.UI.Windows.csproj", "{0A86ED89-1FC5-42AA-925C-4578FA30607A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI.Windows", "src\windows\Atlassian.Bitbucket.UI.Windows\Atlassian.Bitbucket.UI.Windows.csproj", "{3F015046-DAF2-4D2A-96EC-F9782F169E45}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab", "src\shared\GitLab\GitLab.csproj", "{570897DC-A85C-4598-B793-9A00CF710119}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.Tests", "src\shared\GitLab.Tests\GitLab.Tests.csproj", "{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}" @@ -275,16 +273,6 @@ Global {0A86ED89-1FC5-42AA-925C-4578FA30607A}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU {0A86ED89-1FC5-42AA-925C-4578FA30607A}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU {0A86ED89-1FC5-42AA-925C-4578FA30607A}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {3F015046-DAF2-4D2A-96EC-F9782F169E45}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU {570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.Build.0 = Debug|Any CPU {570897DC-A85C-4598-B793-9A00CF710119}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU @@ -361,7 +349,6 @@ Global {8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} {AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9} {0A86ED89-1FC5-42AA-925C-4578FA30607A} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} - {3F015046-DAF2-4D2A-96EC-F9782F169E45} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} {570897DC-A85C-4598-B793-9A00CF710119} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} {83EAC1F9-8E1F-41FC-8FC9-2C452452D64E} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Assets/Styles.xaml b/src/windows/Atlassian.Bitbucket.UI.Windows/Assets/Styles.xaml deleted file mode 100644 index 67fe8a512..000000000 --- a/src/windows/Atlassian.Bitbucket.UI.Windows/Assets/Styles.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Assets/atlassian-logo.png b/src/windows/Atlassian.Bitbucket.UI.Windows/Assets/atlassian-logo.png deleted file mode 100644 index 6226f936d1adeafa210967f8bbf704e1557349ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13455 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 diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Atlassian.Bitbucket.UI.Windows.csproj b/src/windows/Atlassian.Bitbucket.UI.Windows/Atlassian.Bitbucket.UI.Windows.csproj deleted file mode 100644 index f74d8c2b8..000000000 --- a/src/windows/Atlassian.Bitbucket.UI.Windows/Atlassian.Bitbucket.UI.Windows.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - net472 - true - Atlassian.Bitbucket.UI.Windows - Atlassian.Bitbucket.UI - Atlassian.Bitbucket.UI.Windows.Program - - - - - - - - - - - - - - - - diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/CredentialsCommandImpl.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/CredentialsCommandImpl.cs deleted file mode 100644 index ecfc6b891..000000000 --- a/src/windows/Atlassian.Bitbucket.UI.Windows/Commands/CredentialsCommandImpl.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Atlassian.Bitbucket.UI.Commands; -using Atlassian.Bitbucket.UI.ViewModels; -using Atlassian.Bitbucket.UI.Windows.Views; -using GitCredentialManager; -using GitCredentialManager.UI.Windows; - -namespace Atlassian.Bitbucket.UI.Windows.Commands -{ - public class CredentialsCommandImpl : CredentialsCommand - { - public CredentialsCommandImpl(ICommandContext context) : base(context) { } - - protected override Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct) - { - return Gui.ShowDialogWindow(viewModel, () => new CredentialsView(), GetParentHandle()); - } - } -} diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml b/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml deleted file mode 100644 index f221e6f13..000000000 --- a/src/windows/Atlassian.Bitbucket.UI.Windows/Controls/TesterWindow.xaml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/windows/Core.UI.Windows/Controls/WpfDialogWindow.xaml.cs b/src/windows/Core.UI.Windows/Controls/WpfDialogWindow.xaml.cs deleted file mode 100644 index d6fc69f1d..000000000 --- a/src/windows/Core.UI.Windows/Controls/WpfDialogWindow.xaml.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Threading; -using GitCredentialManager.UI.Controls; -using GitCredentialManager.UI.ViewModels; - -namespace GitCredentialManager.UI.Windows.Controls -{ - public partial class WpfDialogWindow : Window - { - private readonly UserControl _view; - - public WpfDialogWindow(UserControl view) - { - InitializeComponent(); - - DataContextChanged += OnDataContextChanged; - - _view = view; - ContentHolder.Content = _view; - } - - private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) - { - if (DataContext is WindowViewModel vm) - { - vm.Accepted += (s, _) => - { - DialogResult = true; - Close(); - }; - - vm.Canceled += (s, _) => - { - DialogResult = false; - Close(); - }; - } - - if (_view is IFocusable focusable) - { - // Send a focus request to the child view on idle - System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, (Action)(() => focusable.SetFocus())); - } - } - - private void CloseButton_Click(object sender, RoutedEventArgs e) - { - if (DataContext is WindowViewModel vm) - { - vm.Cancel(); - } - } - - private void Border_MouseDown(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - DragMove(); - } - } - } -} diff --git a/src/windows/Core.UI.Windows/Converters/BooleanConverters.cs b/src/windows/Core.UI.Windows/Converters/BooleanConverters.cs deleted file mode 100644 index be195a751..000000000 --- a/src/windows/Core.UI.Windows/Converters/BooleanConverters.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Windows; -using System.Windows.Data; - -namespace GitCredentialManager.UI.Windows.Converters -{ - [ValueConversion(typeof(bool), typeof(bool))] - public class BooleanNotConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return !(bool)value; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } - - [ValueConversion(typeof(bool), typeof(bool))] - public class BooleanOrConverter : IMultiValueConverter - { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - return values.Cast().Aggregate(false, (x, y) => x || y); - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - return new[] { Binding.DoNothing }; - } - } - - [ValueConversion(typeof(bool), typeof(Visibility))] - public class BooleanToVisibilityConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return ConverterHelper.GetConditionalVisibility((bool)value, parameter); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } - - [ValueConversion(typeof(bool), typeof(Visibility))] - public class BooleanOrToVisibilityConverter : IMultiValueConverter - { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - bool show = values.Cast().Aggregate(false, (x, y) => x || y); - return ConverterHelper.GetConditionalVisibility(show, parameter); - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - return new[] { Binding.DoNothing }; - } - } - - [ValueConversion(typeof(bool), typeof(Visibility))] - public class BooleanAndToVisibilityConverter : IMultiValueConverter - { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - bool show = values.Cast().Aggregate(true, (x, y) => x && y); - return ConverterHelper.GetConditionalVisibility(show, parameter); - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - return new[] { Binding.DoNothing }; - } - } -} diff --git a/src/windows/Core.UI.Windows/Converters/ConverterUtils.cs b/src/windows/Core.UI.Windows/Converters/ConverterUtils.cs deleted file mode 100644 index bf91e1e78..000000000 --- a/src/windows/Core.UI.Windows/Converters/ConverterUtils.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Windows; - -namespace GitCredentialManager.UI.Windows.Converters -{ - public class ConverterHelper - { - private static char[] s_commaSeparator = new char[] { ',' }; - - /// - /// Returns true if parameter contains the specified option text. - /// - /// comma-separated options - /// option to search - /// true if parameter contains option, false otherwise - private static bool ParameterContains(object parameter, String option) - { - string arg = parameter as string; - if (!string.IsNullOrEmpty(arg)) - { - string[] optionArgs = arg.Split(s_commaSeparator, StringSplitOptions.RemoveEmptyEntries); - foreach (string optionArg in optionArgs) - { - if (optionArg.Equals(option, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - return false; - } - - /// - /// Returns true if parameter contains "Not", "!", or "Invert". - /// - /// comma-separated options - /// true if parameter has the invert option - public static bool GetInvert(object parameter) - { - string arg = parameter as String; - if (!string.IsNullOrEmpty(arg)) - { - string[] options = arg.Split(s_commaSeparator, StringSplitOptions.RemoveEmptyEntries); - foreach (string option in options) - { - if (option.Equals("Not", StringComparison.OrdinalIgnoreCase) || - option.Equals("!", StringComparison.OrdinalIgnoreCase) || - option.Equals("Invert", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - return false; - } - - /// - /// Returns the appropriate Visibility based on the show condition - /// and the preferred Visibility.Collaped or Visibility.Hidden option - /// in parameter. - /// - /// true to get Visibility.Visible, - /// false for Visibility.Collapsed or Visibility.Hidden depending on parameter. - /// comma-separated options. "Not", "!", or "Invert" to invert - /// the bool evaluation before converting to Visibility. "Hidden" to get - /// Visibility.Hidden (where the default is Visibility.Collapsed). - /// Visibility.Collapsed or Visibility.Hidden - public static Visibility GetConditionalVisibility(bool show, object parameter) - { - return GetConditionalVisibility(show, parameter, false); - } - - /// - /// Returns the appropriate Visibility based on the show condition - /// and the preferred Visibility.Collaped or Visibility.Hidden option - /// in parameter. - /// - /// true to get Visibility.Visible, - /// false for Visibility.Collapsed or Visibility.Hidden depending on parameter. - /// comma-separated options. "Not", "!", or "Invert" to invert - /// the bool evaluation before converting to Visibility. "Hidden" to get - /// Visibility.Hidden (where the default is Visibility.Collapsed). - /// true to ignore the Invert option in parameter. - /// This is used to avoid double-inverting. - /// Visibility.Collapsed or Visibility.Hidden - public static Visibility GetConditionalVisibility(bool show, object parameter, bool ignoreInvert) - { - bool result = show; - if (!ignoreInvert && GetInvert(parameter)) - { - result = !show; - } - return result ? Visibility.Visible : GetCollapsedOrHidden(parameter); - } - - /// - /// Returns Visibility.Hidden if parameter contains "Hidden". Default is - /// Visibility.Collapsed. - /// - /// comma-separated options. "Hidden" to get - /// Visibility.Hidden. Default is Visibility.Collapsed. - /// Visibility.Collapsed or Visibility.Hidden - internal static Visibility GetCollapsedOrHidden(object parameter) - { - return ParameterContains(parameter, "Hidden") ? Visibility.Hidden : Visibility.Collapsed; - } - - /// - /// Returns Visibility.Hidden if parameter[argIndex] contains "Hidden". Default is - /// Visibility.Collapsed. - /// - /// parameter[argIndex] as comma-separated options. - /// "Hidden" to get Visibility.Hidden. Default is Visibility.Collapsed. - /// index to the actual argument to use in the parameter String[] - /// Visibility - public static Visibility GetCollapsedOrHiddenFromArray(object parameter, int argIndex) - { - object[] args = parameter as object[]; - if (args != null && args.Length >= argIndex + 1) - { - return GetCollapsedOrHidden(args[argIndex] as string); - } - return Visibility.Collapsed; - } - } -} diff --git a/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs b/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs deleted file mode 100644 index 6fda85bd2..000000000 --- a/src/windows/Core.UI.Windows/Converters/NonEmptyStringToVisibleConverter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace GitCredentialManager.UI.Windows.Converters -{ - [ValueConversion(typeof(string), typeof(Visibility))] - public class NonEmptyStringToVisibleConverter : IValueConverter - { - public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return ConverterHelper.GetConditionalVisibility(!string.IsNullOrEmpty(value as string), parameter); - } - - public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } - - [ValueConversion(typeof(string), typeof(Visibility))] - public class NonNullToVisibleConverter : IValueConverter - { - public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return ConverterHelper.GetConditionalVisibility(value != null, parameter); - } - - public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } -} diff --git a/src/windows/Core.UI.Windows/Core.UI.Windows.csproj b/src/windows/Core.UI.Windows/Core.UI.Windows.csproj deleted file mode 100644 index ba14edca4..000000000 --- a/src/windows/Core.UI.Windows/Core.UI.Windows.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net472 - true - GitCredentialManager.UI.Windows - gcmcoreuiwpf - - - - - - - - - - - diff --git a/src/windows/Core.UI.Windows/Gui.cs b/src/windows/Core.UI.Windows/Gui.cs deleted file mode 100644 index 81508bf9f..000000000 --- a/src/windows/Core.UI.Windows/Gui.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.ComponentModel; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using GitCredentialManager.UI.ViewModels; -using GitCredentialManager.UI.Windows.Controls; - -namespace GitCredentialManager.UI.Windows -{ - public static class Gui - { - /// - /// Present the user with a . - /// - /// factory. - /// Parent window handle. - /// - /// Returns `` if the user completed the dialog; otherwise `` - /// if the user canceled or abandoned the dialog. - /// - public static Task ShowWindow(Func windowCreator, IntPtr parentHwnd) - { - return StartSTATask(() => ShowDialog(windowCreator(), parentHwnd)); - } - - /// - /// Present the user with a . - /// - /// - /// Returns `` if the user completed the dialog and the view model is valid; - /// otherwise `` if the user canceled or abandoned the dialog, or the view - /// model is invalid. - /// - /// Window view model. - /// Window content factory. - /// Parent window handle. - public static Task ShowDialogWindow(WindowViewModel viewModel, Func contentCreator, IntPtr parentHwnd) - { - return ShowWindow(() => new WpfDialogWindow(contentCreator()) { DataContext = viewModel }, parentHwnd); - } - - private static Task StartSTATask(Action action) - { - var completionSource = new TaskCompletionSource(); - var thread = new Thread(() => - { - try - { - action(); - completionSource.SetResult(null); - } - catch (Exception e) - { - completionSource.SetException(e); - } - }); - - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - - return completionSource.Task; - } - - public static bool? ShowDialog(Window window, IntPtr parentHwnd) - { - // Zero is not a valid window handle - if (parentHwnd == IntPtr.Zero) - { - return window.ShowDialog(); - } - - // Set the parent window handle and ensure the dialog starts in the correct location - new System.Windows.Interop.WindowInteropHelper(window).Owner = parentHwnd; - window.WindowStartupLocation = WindowStartupLocation.CenterOwner; - - const int ERROR_INVALID_WINDOW_HANDLE = 1400; - - try - { - return window.ShowDialog(); - } - catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_INVALID_WINDOW_HANDLE) - { - // The window handle given was invalid - clear the owner and show the dialog centered on the screen - window.Owner = null; - window.WindowStartupLocation = WindowStartupLocation.CenterScreen; - return window.ShowDialog(); - } - } - } -} diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj index 936bdb98e..99253c445 100644 --- a/src/windows/Installer.Windows/Installer.Windows.csproj +++ b/src/windows/Installer.Windows/Installer.Windows.csproj @@ -11,7 +11,6 @@ - From 5c50a73508a2cc63a13991e46e61526b1189a6aa Mon Sep 17 00:00:00 2001 From: Massimo Giambona Date: Tue, 12 Sep 2023 13:38:07 +0000 Subject: [PATCH 19/45] Support linux install path from src --- .../Packaging.Linux/Packaging.Linux.csproj | 4 ++-- src/linux/Packaging.Linux/build.sh | 9 +++++++-- .../Packaging.Linux/install-from-source.sh | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj index b9fb5ccd5..03189d26d 100644 --- a/src/linux/Packaging.Linux/Packaging.Linux.csproj +++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 3b179e22d..b6861409a 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -30,6 +30,10 @@ case "$i" in INSTALL_FROM_SOURCE="${i#*=}" shift # past argument=value ;; + --install-location=*) + INSTALL_LOCATION="${i#*=}" + shift # past argument=value + ;; *) # unknown option ;; @@ -50,10 +54,11 @@ SYMBOLS="$OUTDIR/payload.sym" "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || exit 1 if [ $INSTALL_FROM_SOURCE = true ]; then - INSTALL_LOCATION="/usr/local" + #INSTALL_LOCATION="/usr/local" + mkdir -p "$INSTALL_LOCATION" - echo "Installing..." + echo "Installing to $INSTALL_LOCATION" # Install directories INSTALL_TO="$INSTALL_LOCATION/share/gcm-core/" diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index e379a55f5..ab86159a3 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -6,6 +6,7 @@ # for additional details. set -e +installLocation=/usr/local is_ci= for i in "$@"; do case "$i" in @@ -13,15 +14,26 @@ for i in "$@"; do is_ci=true shift # Past argument=value ;; + --install-location=*) + installLocation="${i#*=}" + shift # past argument=value + ;; esac done + +# If pass the install-location check if it exists +if [! -d "$installLocation" ]; then + echo "The folder $installLocation do not exists" + exit +fi + # In non-ci scenarios, advertise what we will be doing and # give user the option to exit. if [ -z $is_ci ]; then echo "This script will download, compile, and install Git Credential Manager to: - /usr/local/bin + $installLocation/bin Git Credential Manager is licensed under the MIT License: https://aka.ms/gcm/license" @@ -225,5 +237,5 @@ if [ -z "$DOTNET_ROOT" ]; then fi cd "$toplevel_path" -$sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -add_to_PATH "/usr/local/bin" +$sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:InstallLocation=$installLocation +add_to_PATH $installLocation From 53e2b48afb0c0a202b86f72d17ad8412b1875c06 Mon Sep 17 00:00:00 2001 From: Lessley Date: Mon, 18 Sep 2023 14:18:24 -0600 Subject: [PATCH 20/45] install from source: custom install location edits Update custom install to define a default path in `Packaging.Linux.csproj`. Include a few spelling/grammar/refactoring tweaks as well. --- src/linux/Packaging.Linux/Packaging.Linux.csproj | 1 + src/linux/Packaging.Linux/build.sh | 9 +++++---- src/linux/Packaging.Linux/install-from-source.sh | 9 ++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj index 03189d26d..c0a30e608 100644 --- a/src/linux/Packaging.Linux/Packaging.Linux.csproj +++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj @@ -9,6 +9,7 @@ false + /usr/local diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index b6861409a..98e5c2f6e 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -40,6 +40,11 @@ case "$i" in esac done +# Ensure install location exists +if [! -d "$installLocation" ]; then + mkdir -p "$INSTALL_LOCATION" +fi + # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" if [ -z "$VERSION" ]; then @@ -54,10 +59,6 @@ SYMBOLS="$OUTDIR/payload.sym" "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || exit 1 if [ $INSTALL_FROM_SOURCE = true ]; then - #INSTALL_LOCATION="/usr/local" - - mkdir -p "$INSTALL_LOCATION" - echo "Installing to $INSTALL_LOCATION" # Install directories diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index ab86159a3..f736514de 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -6,7 +6,6 @@ # for additional details. set -e -installLocation=/usr/local is_ci= for i in "$@"; do case "$i" in @@ -21,10 +20,14 @@ for i in "$@"; do esac done +# If install-location is not passed, use default value +if [ -z "$installLocation" ]; then + installLocation=/usr/local +fi -# If pass the install-location check if it exists +# Ensure install location exists if [! -d "$installLocation" ]; then - echo "The folder $installLocation do not exists" + echo "The folder $installLocation does not exist" exit fi From 03a23a110792be95b0682c5f53447fcc2ed049cc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 18 Sep 2023 14:53:51 -0700 Subject: [PATCH 21/45] docs: update dev.azure.com product page URLs Update the URL for the Azure DevOps product page. This used to be just https://dev.azure.com, but this now redirects you to https://azure.microsoft.com/en-us/products/devops instead. The markdown link linter times out following the redirect, so let's just update the original link. --- README.md | 2 +- docs/windows-broker.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a9663e44..18c9b1309 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ We're [MIT][gcm-license] licensed. When using GitHub logos, please be sure to follow the [GitHub logo guidelines][github-logos]. -[azure-devops]: https://dev.azure.com/ +[azure-devops]: https://azure.microsoft.com/en-us/products/devops [azure-devops-ssh]: https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops [bitbucket]: https://bitbucket.org [bitbucket-ssh]: https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html diff --git a/docs/windows-broker.md b/docs/windows-broker.md index 1cc42f222..bfe5afcb0 100644 --- a/docs/windows-broker.md +++ b/docs/windows-broker.md @@ -216,7 +216,7 @@ In order to fix the problem, there are a few options: [azure-refresh-token-terms]: https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components [azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview -[azure-devops]: https://dev.azure.com +[azure-devops]: https://azure.microsoft.com/en-us/products/devops [GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER-experimental [GCM_MSAUTH_USEDEFAULTACCOUNT]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental [credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker-experimental From c99ef6dde17068d8dfda8d491c7248fa8fb1be66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:32:15 +0000 Subject: [PATCH 22/45] build(deps): bump DavidAnson/markdownlint-cli2-action Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 11.0.0 to 12.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/8f3516061301755c97ff833a8e933f09282cc5b5...3aaa38e446fbd2c288af4291aa0f55d64651050f) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 34574458f..e552891bb 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: DavidAnson/markdownlint-cli2-action@8f3516061301755c97ff833a8e933f09282cc5b5 + - uses: DavidAnson/markdownlint-cli2-action@3aaa38e446fbd2c288af4291aa0f55d64651050f with: globs: | "**/*.md" From 8f925d31be79c2c684409cf22344edc00306ac6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:40:38 +0000 Subject: [PATCH 23/45] build(deps): bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/continuous-integration.yml | 6 ++-- .github/workflows/lint-docs.yml | 4 +-- .github/workflows/release.yml | 28 +++++++++---------- .../validate-install-from-source.yml | 2 +- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 06a331d98..f9b3edea2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: language: [ 'csharp' ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index d80df6885..7562447fc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3.2.0 @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3.2.0 @@ -97,7 +97,7 @@ jobs: runtime: [ osx-x64, osx-arm64 ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3.2.0 diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index e552891bb..160886d09 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -18,7 +18,7 @@ jobs: name: Lint markdown files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DavidAnson/markdownlint-cli2-action@3aaa38e446fbd2c288af4291aa0f55d64651050f with: @@ -30,7 +30,7 @@ jobs: name: Check for broken links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run link checker # For any troubleshooting, see: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa94491e7..2d0cd7bb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: matrix: runtime: [ osx-x64, osx-arm64 ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up dotnet uses: actions/setup-dotnet@v3.2.0 @@ -77,7 +77,7 @@ jobs: runtime: [ osx-x64, osx-arm64 ] needs: osx-build steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download payload uses: actions/download-artifact@v3 @@ -136,7 +136,7 @@ jobs: runtime: [ osx-x64, osx-arm64 ] needs: osx-payload-sign steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set version environment variable run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV @@ -180,7 +180,7 @@ jobs: runtime: [ osx-x64, osx-arm64 ] needs: osx-pack steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download unsigned package uses: actions/download-artifact@v3 @@ -247,7 +247,7 @@ jobs: runs-on: windows-latest environment: release steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up dotnet uses: actions/setup-dotnet@v3.2.0 @@ -348,7 +348,7 @@ jobs: name: Build Linux runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3.2.0 @@ -381,7 +381,7 @@ jobs: runs-on: windows-latest environment: release steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download artifacts uses: actions/download-artifact@v3 @@ -435,7 +435,7 @@ jobs: name: Build .NET tool runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3.2.0 @@ -460,7 +460,7 @@ jobs: environment: release needs: dotnet-tool-build steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download payload uses: actions/download-artifact@v3 @@ -517,12 +517,12 @@ jobs: runs-on: ubuntu-latest needs: dotnet-tool-payload-sign steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set version environment variable run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download signed payload uses: actions/download-artifact@v3 @@ -554,7 +554,7 @@ jobs: environment: release needs: dotnet-tool-pack steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download unsigned package uses: actions/download-artifact@v3 @@ -638,7 +638,7 @@ jobs: runs-on: ${{ matrix.component.os }} needs: [ osx-sign, win-sign, linux-sign, dotnet-tool-sign ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download artifacts uses: actions/download-artifact@v3 @@ -700,7 +700,7 @@ jobs: environment: release needs: [ validate ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set version environment variable run: | diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml index 34335698b..ff28dc85c 100644 --- a/.github/workflows/validate-install-from-source.yml +++ b/.github/workflows/validate-install-from-source.yml @@ -36,7 +36,7 @@ jobs: dnf install which -y fi - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | sh "${GITHUB_WORKSPACE}/src/linux/Packaging.Linux/install-from-source.sh" -y From 1e904d1035641393ce87c8297670574ff0197f32 Mon Sep 17 00:00:00 2001 From: Matthew Cheetham Date: Mon, 11 Sep 2023 14:47:27 -0700 Subject: [PATCH 24/45] msauth: replace WPF progress window with Avalonia --- .../Authentication/MicrosoftAuthentication.cs | 96 ++----------------- .../Core/UI/Controls/ProgressWindow.axaml | 14 +++ .../Core/UI/Controls/ProgressWindow.axaml.cs | 37 +++++++ 3 files changed, 61 insertions(+), 86 deletions(-) create mode 100644 src/shared/Core/UI/Controls/ProgressWindow.axaml create mode 100644 src/shared/Core/UI/Controls/ProgressWindow.axaml.cs diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 0dd0fefa4..b39cc1a73 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -11,13 +11,12 @@ using System.Text; using System.Threading; using GitCredentialManager.UI; +using GitCredentialManager.UI.Controls; using GitCredentialManager.UI.ViewModels; using GitCredentialManager.UI.Views; using Microsoft.Identity.Client.AppConfig; #if NETFRAMEWORK -using System.Drawing; -using System.Windows.Forms; using Microsoft.Identity.Client.Broker; #endif @@ -118,10 +117,6 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat "live", "liveconnect", "liveid", }; -#if NETFRAMEWORK - private DummyWindow _dummyWindow; -#endif - public MicrosoftAuthentication(ICommandContext context) : base(context) { } @@ -130,6 +125,8 @@ public MicrosoftAuthentication(ICommandContext context) public async Task GetTokenForUserAsync( string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt) { + var uiCts = new CancellationTokenSource(); + // Check if we can and should use OS broker authentication bool useBroker = CanUseBroker(); Context.Trace.WriteLine(useBroker @@ -144,7 +141,7 @@ public MicrosoftAuthentication(ICommandContext context) try { // Create the public client application for authentication - IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker, msaPt); + IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker, msaPt, uiCts); AuthenticationResult result = null; @@ -261,10 +258,8 @@ public MicrosoftAuthentication(ICommandContext context) } finally { -#if NETFRAMEWORK - // If we created a dummy window during authentication we should dispose of it now that we're done - _dummyWindow?.Dispose(); -#endif + // If we created some global UI (e.g. progress) during authentication we should dismiss them now that we're done + uiCts.Cancel(); } } @@ -451,8 +446,8 @@ internal MicrosoftAuthenticationFlowType GetFlowType() } } - private async Task CreatePublicClientApplicationAsync( - string authority, string clientId, Uri redirectUri, bool enableBroker, bool msaPt) + private async Task CreatePublicClientApplicationAsync(string authority, + string clientId, Uri redirectUri, bool enableBroker, bool msaPt, CancellationTokenSource uiCts) { var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); @@ -495,11 +490,8 @@ internal MicrosoftAuthenticationFlowType GetFlowType() } else if (enableBroker) // Only actually need to set a parent window when using the Windows broker { -#if NETFRAMEWORK - Context.Trace.WriteLine($"Using dummy parent window for MSAL authentication dialogs."); - _dummyWindow = new DummyWindow(); - appBuilder.WithParentActivityOrWindow(_dummyWindow.ShowAndGetHandle); -#endif + Context.Trace.WriteLine("Using progress parent window for MSAL authentication dialogs."); + appBuilder.WithParentActivityOrWindow(() => ProgressWindow.ShowAndGetHandle(uiCts.Token)); } } } @@ -899,73 +891,5 @@ public MsalResult(AuthenticationResult msalResult) public string AccessToken => _msalResult.AccessToken; public string AccountUpn => _msalResult.Account?.Username; } - -#if NETFRAMEWORK - private class DummyWindow : IDisposable - { - private readonly Thread _staThread; - private readonly ManualResetEventSlim _readyEvent; - private Form _window; - private IntPtr _handle; - - public DummyWindow() - { - _staThread = new Thread(ThreadProc); - _staThread.SetApartmentState(ApartmentState.STA); - _readyEvent = new ManualResetEventSlim(); - } - - public IntPtr ShowAndGetHandle() - { - _staThread.Start(); - _readyEvent.Wait(); - return _handle; - } - - public void Dispose() - { - _window?.Invoke(() => _window.Close()); - - if (_staThread.IsAlive) - { - _staThread.Join(); - } - } - - private void ThreadProc() - { - System.Windows.Forms.Application.EnableVisualStyles(); - _window = new Form - { - TopMost = true, - ControlBox = false, - MaximizeBox = false, - MinimizeBox = false, - ClientSize = new Size(182, 46), - FormBorderStyle = FormBorderStyle.None, - StartPosition = FormStartPosition.CenterScreen, - }; - - var progress = new ProgressBar - { - Style = ProgressBarStyle.Marquee, - Location = new Point(12, 12), - Size = new Size(158, 23), - MarqueeAnimationSpeed = 30, - }; - - _window.Controls.Add(progress); - _window.Shown += (s, e) => - { - _handle = _window.Handle; - _readyEvent.Set(); - }; - - _window.ShowDialog(); - _window.Dispose(); - _window = null; - } - } -#endif } } diff --git a/src/shared/Core/UI/Controls/ProgressWindow.axaml b/src/shared/Core/UI/Controls/ProgressWindow.axaml new file mode 100644 index 000000000..3bfc20f5c --- /dev/null +++ b/src/shared/Core/UI/Controls/ProgressWindow.axaml @@ -0,0 +1,14 @@ + + + diff --git a/src/shared/Core/UI/Controls/ProgressWindow.axaml.cs b/src/shared/Core/UI/Controls/ProgressWindow.axaml.cs new file mode 100644 index 000000000..7dc46dac6 --- /dev/null +++ b/src/shared/Core/UI/Controls/ProgressWindow.axaml.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GitCredentialManager.UI.Controls; + +public partial class ProgressWindow : Window +{ + public ProgressWindow() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public static IntPtr ShowAndGetHandle(CancellationToken ct) + { + var tsc = new TaskCompletionSource(); + + Window CreateWindow() + { + var window = new ProgressWindow(); + window.Loaded += (s, e) => tsc.SetResult(window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero); + return window; + } + + Task _ = AvaloniaUi.ShowWindowAsync(CreateWindow, IntPtr.Zero, ct); + + return tsc.Task.GetAwaiter().GetResult(); + } +} From 24c1883a6aff303636a314425b1c03a3aaa4550c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:48:24 +0000 Subject: [PATCH 25/45] build(deps): bump DavidAnson/markdownlint-cli2-action Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 12.0.0 to 13.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/3aaa38e446fbd2c288af4291aa0f55d64651050f...ed4dec634fd2ef689c7061d5647371d8248064f1) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 160886d09..cbc4b241e 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@3aaa38e446fbd2c288af4291aa0f55d64651050f + - uses: DavidAnson/markdownlint-cli2-action@ed4dec634fd2ef689c7061d5647371d8248064f1 with: globs: | "**/*.md" From 3b6b07dfaaa08d6aa2e2e64099afd36c55ba364c Mon Sep 17 00:00:00 2001 From: Lessley Date: Mon, 25 Sep 2023 10:26:46 -0600 Subject: [PATCH 26/45] install from source: refactor install location Refactor references to "install location" to instead reference "install prefix," since we use this value as a prefix for multiple paths (rather than a static location). --- .../Packaging.Linux/Packaging.Linux.csproj | 6 ++--- src/linux/Packaging.Linux/build.sh | 16 +++++++------- .../Packaging.Linux/install-from-source.sh | 22 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj index c0a30e608..362ffc230 100644 --- a/src/linux/Packaging.Linux/Packaging.Linux.csproj +++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj @@ -9,7 +9,7 @@ false - /usr/local + /usr/local @@ -24,8 +24,8 @@ - - + + diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 98e5c2f6e..6672857d2 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -30,8 +30,8 @@ case "$i" in INSTALL_FROM_SOURCE="${i#*=}" shift # past argument=value ;; - --install-location=*) - INSTALL_LOCATION="${i#*=}" + --install-prefix=*) + INSTALL_PREFIX="${i#*=}" shift # past argument=value ;; *) @@ -40,9 +40,9 @@ case "$i" in esac done -# Ensure install location exists -if [! -d "$installLocation" ]; then - mkdir -p "$INSTALL_LOCATION" +# Ensure install prefix exists +if [! -d "$INSTALL_PREFIX" ]; then + mkdir -p "$INSTALL_PREFIX" fi # Perform pre-execution checks @@ -59,11 +59,11 @@ SYMBOLS="$OUTDIR/payload.sym" "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || exit 1 if [ $INSTALL_FROM_SOURCE = true ]; then - echo "Installing to $INSTALL_LOCATION" + echo "Installing to $INSTALL_PREFIX" # Install directories - INSTALL_TO="$INSTALL_LOCATION/share/gcm-core/" - LINK_TO="$INSTALL_LOCATION/bin/" + INSTALL_TO="$INSTALL_PREFIX/share/gcm-core/" + LINK_TO="$INSTALL_PREFIX/bin/" mkdir -p "$INSTALL_TO" "$LINK_TO" diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index f736514de..58a529b09 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -13,21 +13,21 @@ for i in "$@"; do is_ci=true shift # Past argument=value ;; - --install-location=*) - installLocation="${i#*=}" + --install-prefix=*) + installPrefix="${i#*=}" shift # past argument=value ;; esac done -# If install-location is not passed, use default value -if [ -z "$installLocation" ]; then - installLocation=/usr/local +# If install-prefix is not passed, use default value +if [ -z "$installPrefix" ]; then + installPrefix=/usr/local fi -# Ensure install location exists -if [! -d "$installLocation" ]; then - echo "The folder $installLocation does not exist" +# Ensure install directory exists +if [! -d "$installPrefix" ]; then + echo "The folder $installPrefix does not exist" exit fi @@ -36,7 +36,7 @@ fi if [ -z $is_ci ]; then echo "This script will download, compile, and install Git Credential Manager to: - $installLocation/bin + $installPrefix/bin Git Credential Manager is licensed under the MIT License: https://aka.ms/gcm/license" @@ -240,5 +240,5 @@ if [ -z "$DOTNET_ROOT" ]; then fi cd "$toplevel_path" -$sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:InstallLocation=$installLocation -add_to_PATH $installLocation +$sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:installPrefix=$installPrefix +add_to_PATH $installPrefix From 720d34751323bf565670393ac06b84518cb6a5e6 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Mon, 25 Sep 2023 16:39:28 +0000 Subject: [PATCH 27/45] install from source: correct path addition Ensure the correct path for GCM is added to `PATH` at the end of the `install-from-source` script. --- src/linux/Packaging.Linux/install-from-source.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index 58a529b09..98fe7bc4d 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -241,4 +241,4 @@ fi cd "$toplevel_path" $sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:installPrefix=$installPrefix -add_to_PATH $installPrefix +add_to_PATH "$installPrefix/bin" From e1064f3bac0326ced158a21a43453ee1dc00a3ee Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 26 Sep 2023 11:43:50 -0700 Subject: [PATCH 28/45] avalonia: remove workaround for devtools and app lifetime The issue https://github.com/AvaloniaUI/Avalonia/issues/10296 has been fixed so we can now remove this workaround. --- src/shared/Core/UI/AvaloniaUi.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/shared/Core/UI/AvaloniaUi.cs b/src/shared/Core/UI/AvaloniaUi.cs index 65b681884..477979ae6 100644 --- a/src/shared/Core/UI/AvaloniaUi.cs +++ b/src/shared/Core/UI/AvaloniaUi.cs @@ -53,15 +53,7 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext, #else .UsePlatformDetect() #endif - .LogToTrace() - // Workaround https://github.com/AvaloniaUI/Avalonia/issues/10296 - // by always setting a application lifetime. - .SetupWithLifetime( - new ClassicDesktopStyleApplicationLifetime - { - ShutdownMode = ShutdownMode.OnExplicitShutdown - } - ); + .LogToTrace(); appInitialized.Set(); From f4f36896c1e57bf4060cac419fbcc5576c623177 Mon Sep 17 00:00:00 2001 From: M Hickford Date: Tue, 3 Oct 2023 09:38:21 +0100 Subject: [PATCH 29/45] recognise GitLab hosts by WWW-Authenticate header --- src/shared/GitLab/GitLabHostProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs index decd1d8df..eda6e2f0f 100644 --- a/src/shared/GitLab/GitLabHostProvider.cs +++ b/src/shared/GitLab/GitLabHostProvider.cs @@ -4,6 +4,7 @@ using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; using System.Net.Http.Headers; +using System.Linq; namespace GitLab { @@ -69,6 +70,11 @@ public override bool IsSupported(InputArguments input) return true; } + if (input.WwwAuth.Any(x => x.Contains("realm=\"GitLab\""))) + { + return true; + } + return false; } From 531d7ed5a4071379d17cef8a6fd19afe30ec70d0 Mon Sep 17 00:00:00 2001 From: Lessley Date: Thu, 5 Oct 2023 18:21:05 -0600 Subject: [PATCH 30/45] release: change dotnet to .NET and loosen version requirements Update .NET setup tasks to be titled "Set up .NET" instead of "Setup dotnet." Additionally, change from using a specific .NET version (6.0.201) to a more general version that aligns with the current version in use by the project (7.0.x). --- .github/workflows/release.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d0cd7bb5..b7e9e3e3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up dotnet + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Install dependencies run: dotnet restore @@ -141,10 +141,10 @@ jobs: - name: Set version environment variable run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - name: Set up dotnet + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Download signed payload uses: actions/download-artifact@v3 @@ -249,10 +249,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up dotnet + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Install dependencies run: dotnet restore @@ -350,10 +350,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Install dependencies run: dotnet restore @@ -437,10 +437,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Build .NET tool run: | @@ -530,10 +530,10 @@ jobs: name: dotnet-tool-payload-sign path: signed - - name: Setup .NET + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Package tool run: | @@ -707,10 +707,10 @@ jobs: # Remove the "revision" portion of the version echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - name: Set up dotnet + - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 6.0.201 + dotnet-version: 7.0.x - name: Download artifacts uses: actions/download-artifact@v3 From 556230748125990e1f53b1574ebad93a6389590e Mon Sep 17 00:00:00 2001 From: Lessley Date: Thu, 5 Oct 2023 18:04:28 -0600 Subject: [PATCH 31/45] release: add permissions and prereqs Add required permissions for federated credentials [1] to the release workflow. Additionally, add a prereqs job to set the GCM version for the workflow. 1: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure#adding-permissions-settings --- .github/workflows/release.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7e9e3e3e..bd7e2e1d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,23 @@ name: release on: workflow_dispatch: +permissions: + id-token: write + contents: write + jobs: + prereqs: + name: Prerequisites + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Set version + run: echo "version=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_OUTPUT + id: version + # ================================ # macOS # ================================ @@ -698,15 +714,10 @@ jobs: name: Publish GitHub draft release runs-on: ubuntu-latest environment: release - needs: [ validate ] + needs: [ prereqs, validate ] steps: - uses: actions/checkout@v4 - - name: Set version environment variable - run: | - # Remove the "revision" portion of the version - echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - name: Set up .NET uses: actions/setup-dotnet@v3.2.0 with: @@ -736,7 +747,7 @@ jobs: script: | const fs = require('fs'); const path = require('path'); - const version = process.env.VERSION + const version = "${{ needs.prereqs.outputs.version }}" var releaseMetadata = { owner: context.repo.owner, From 27d763692d139d77bcb3599e144b3044a6cb7226 Mon Sep 17 00:00:00 2001 From: Lessley Date: Fri, 6 Oct 2023 20:48:13 -0600 Subject: [PATCH 32/45] release: update Linux to use GPG signing Update the Linux component of the release workflow to use GPG signing instead of ESRP. --- .github/workflows/release.yml | 121 +++++++++++++++------------------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd7e2e1d4..8957d18e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -360,9 +360,11 @@ jobs: # ================================ # Linux # ================================ - linux-build: - name: Build Linux + create-linux-artifacts: + name: Create Linux Artifacts runs-on: ubuntu-latest + environment: release + needs: prereqs steps: - uses: actions/checkout@v4 @@ -371,78 +373,68 @@ jobs: with: dotnet-version: 7.0.x - - name: Install dependencies - run: dotnet restore - - name: Build run: dotnet build --configuration=LinuxRelease - - name: Lay out + - name: Run Linux unit tests run: | - mkdir -p linux-build/deb linux-build/tar - mv out/linux/Packaging.Linux/Release/deb/*.deb linux-build/deb - mv out/linux/Packaging.Linux/Release/tar/*.tar.gz linux-build/tar + dotnet test --configuration=LinuxRelease - - name: Upload artifacts - uses: actions/upload-artifact@v3 + - name: Log into Azure + uses: azure/login@v1 with: - name: linux-build - path: | - linux-build + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - linux-sign: - name: Sign Linux tarball and Debian package - needs: linux-build - # ESRP service requires signing to run on Windows - runs-on: windows-latest - environment: release - steps: - - uses: actions/checkout@v4 + - name: Prepare for GPG signing + env: + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + GPG_KEY_SECRET_NAME: ${{ secrets.GPG_KEY_SECRET_NAME }} + GPG_PASSPHRASE_SECRET_NAME: ${{ secrets.GPG_PASSPHRASE_SECRET_NAME }} + GPG_KEYGRIP_SECRET_NAME: ${{ secrets.GPG_KEYGRIP_SECRET_NAME }} + run: | + # Install debsigs + sudo apt install debsigs - - name: Download artifacts - uses: actions/download-artifact@v3 - with: - name: linux-build + # Download GPG key, passphrase, and keygrip from Azure Key Vault + key=$(az keyvault secret show --name $GPG_KEY_SECRET_NAME --vault-name $AZURE_VAULT --query "value") + passphrase=$(az keyvault secret show --name $GPG_PASSPHRASE_SECRET_NAME --vault-name $AZURE_VAULT --query "value") + keygrip=$(az keyvault secret show --name $GPG_KEYGRIP_SECRET_NAME --vault-name $AZURE_VAULT --query "value") - - name: Remove symbols - run: | - rm tar/*symbols* + # Remove quotes from downloaded values + key=$(sed -e 's/^"//' -e 's/"$//' <<<"$key") + passphrase=$(sed -e 's/^"//' -e 's/"$//' <<<"$passphrase") + keygrip=$(sed -e 's/^"//' -e 's/"$//' <<<"$keygrip") - - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + # Import GPG key + echo "$key" | base64 -d | gpg --import --no-tty --batch --yes - - name: Set up ESRP client - shell: pwsh - env: - AZURE_VAULT: ${{ secrets.AZURE_VAULT }} - AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} - REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} - run: | - .github\set_up_esrp.ps1 + # Configure GPG + echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf + gpg-connect-agent RELOADAGENT /bye + /usr/lib/gnupg2/gpg-preset-passphrase --preset "$keygrip" <<<"$passphrase" - - name: Run ESRP client - shell: pwsh - env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - LINUX_KEY_CODE: ${{ secrets.LINUX_KEY_CODE }} - LINUX_OP_CODE: ${{ secrets.LINUX_OPERATION_CODE }} + - name: Sign Debian package and tarball run: | - python .github/run_esrp_signing.py deb $env:LINUX_KEY_CODE $env:LINUX_OP_CODE - python .github/run_esrp_signing.py tar $env:LINUX_KEY_CODE $env:LINUX_OP_CODE + # Sign Debian package + version=${{ needs.prereqs.outputs.version }} + mv out/linux/Packaging.Linux/Release/deb/gcm-linux_amd64.$version.deb . + debsigs --sign=origin --verify --check gcm-linux_amd64.$version.deb - - name: Re-name tarball signature file - shell: bash - run: | - signaturepath=$(find signed/*.tar.gz) - mv "$signaturepath" "${signaturepath%.tar.gz}.asc" + # Generate tarball signature file + mv -v out/linux/Packaging.Linux/Release/tar/* . + gpg --batch --yes --armor --output gcm-linux_amd64.$version.tar.gz.asc \ + --detach-sig gcm-linux_amd64.$version.tar.gz - - name: Upload signed tarball and Debian package + - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: linux-sign + name: linux-artifacts path: | - signed + ./*.deb + ./*.asc + ./*.tar.gz # ================================ # .NET Tool @@ -628,13 +620,9 @@ jobs: matrix: component: - os: ubuntu-latest - artifact: linux-sign - command: git-credential-manager - description: debian - - os: ubuntu-latest - artifact: linux-build + artifact: linux-artifacts command: git-credential-manager - description: tarball + description: linux - os: macos-latest artifact: osx-x64-sign command: git-credential-manager @@ -652,7 +640,7 @@ jobs: command: git-credential-manager description: dotnet-tool runs-on: ${{ matrix.component.os }} - needs: [ osx-sign, win-sign, linux-sign, dotnet-tool-sign ] + needs: [ osx-sign, win-sign, create-linux-artifacts, dotnet-tool-sign ] steps: - uses: actions/checkout@v4 @@ -672,14 +660,14 @@ jobs: } - name: Install Linux (Debian package) - if: contains(matrix.component.description, 'debian') + if: contains(matrix.component.description, 'linux') run: | debpath=$(find ./*.deb) sudo apt install $debpath "${{ matrix.component.command }}" configure - name: Install Linux (tarball) - if: contains(matrix.component.description, 'tarball') + if: contains(matrix.component.description, 'linux') run: | # Ensure we find only the source tarball, not the symbols tarpath=$(find ./tar -name '*[[:digit:]].tar.gz') @@ -797,8 +785,7 @@ jobs: uploadDirectoryToRelease('osx-payload-and-symbols'), // Upload Linux artifacts - uploadDirectoryToRelease('linux-build/tar'), - uploadDirectoryToRelease('linux-sign'), + uploadDirectoryToRelease('linux-artifacts'), // Upload .NET tool package uploadDirectoryToRelease('dotnet-tool-sign'), From 16e628a321048080752c10a7184b4c9d624647a9 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Wed, 11 Oct 2023 20:32:33 -0600 Subject: [PATCH 33/45] release: update macOS signing Update macOS component of release workflow to use GitHub certificates for signing and notarization. --- .github/workflows/release.yml | 272 +++++------------- .../osx/Installer.Mac/codesign.sh | 10 +- src/osx/Installer.Mac/dist.sh | 5 + src/osx/Installer.Mac/notarize.sh | 35 +++ 4 files changed, 123 insertions(+), 199 deletions(-) rename .github/run_developer_signing.sh => src/osx/Installer.Mac/codesign.sh (90%) create mode 100755 src/osx/Installer.Mac/notarize.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8957d18e4..e873dc41c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,12 +21,13 @@ jobs: id: version # ================================ -# macOS +# macOS # ================================ - osx-build: - name: Build macOS + create-macos-artifacts: + name: Create macOS artifacts runs-on: macos-latest environment: release + needs: prereqs strategy: matrix: runtime: [ osx-x64, osx-arm64 ] @@ -38,9 +39,6 @@ jobs: with: dotnet-version: 7.0.x - - name: Install dependencies - run: dotnet restore - - name: Build run: | dotnet build src/osx/Installer.Mac/*.csproj \ @@ -57,203 +55,88 @@ jobs: --configuration=MacRelease --output=payload \ --symbol-output=symbols --runtime=${{ matrix.runtime }} - - name: Create keychain + - name: Set up signing/notarization infrastructure env: - CERT_BASE64: ${{ secrets.DEVELOPER_CERTIFICATE_BASE64 }} - CERT_PASSPHRASE: ${{ secrets.DEVELOPER_CERTIFICATE_PASSWORD }} - run: | + A1: ${{ secrets.APPLICATION_CERTIFICATE_BASE64 }} + A2: ${{ secrets.APPLICATION_CERTIFICATE_PASSWORD }} + I1: ${{ secrets.INSTALLER_CERTIFICATE_BASE64 }} + I2: ${{ secrets.INSTALLER_CERTIFICATE_PASSWORD }} + N1: ${{ secrets.APPLE_TEAM_ID }} + N2: ${{ secrets.APPLE_DEVELOPER_ID }} + N3: ${{ secrets.APPLE_DEVELOPER_PASSWORD }} + N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }} + run: | + echo "Setting up signing certificates" security create-keychain -p pwd $RUNNER_TEMP/buildagent.keychain security default-keychain -s $RUNNER_TEMP/buildagent.keychain security unlock-keychain -p pwd $RUNNER_TEMP/buildagent.keychain - echo $CERT_BASE64 | base64 -D > $RUNNER_TEMP/cert.p12 - security import $RUNNER_TEMP/cert.p12 -k $RUNNER_TEMP/buildagent.keychain -P $CERT_PASSPHRASE -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $RUNNER_TEMP/buildagent.keychain - - - name: Developer sign - env: - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - .github/run_developer_signing.sh payload $APPLE_TEAM_ID $GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml - - - name: Upload macOS artifacts - uses: actions/upload-artifact@v3 - with: - name: tmp.${{ matrix.runtime }}-build - path: | - payload - symbols - - osx-payload-sign: - name: Sign macOS payload - # ESRP service requires signing to run on Windows - runs-on: windows-latest - environment: release - strategy: - matrix: - runtime: [ osx-x64, osx-arm64 ] - needs: osx-build - steps: - - uses: actions/checkout@v4 - - - name: Download payload - uses: actions/download-artifact@v3 - with: - name: tmp.${{ matrix.runtime }}-build - - - name: Zip unsigned payload - shell: pwsh - run: | - Compress-Archive -Path payload payload/payload.zip - cd payload - Get-ChildItem -Exclude payload.zip | Remove-Item -Recurse -Force - - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Set up ESRP client - shell: pwsh - env: - AZURE_VAULT: ${{ secrets.AZURE_VAULT }} - AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} - REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} - run: | - .github\set_up_esrp.ps1 - - - name: Run ESRP client - shell: pwsh + echo $A1 | base64 -D > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 \ + -k $RUNNER_TEMP/buildagent.keychain \ + -P $A2 \ + -T /usr/bin/codesign + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s -k pwd \ + $RUNNER_TEMP/buildagent.keychain + + echo $I1 | base64 -D > $RUNNER_TEMP/cert.p12 + security import $RUNNER_TEMP/cert.p12 \ + -k $RUNNER_TEMP/buildagent.keychain \ + -P $I2 \ + -T /usr/bin/productbuild + security set-key-partition-list \ + -S apple-tool:,apple:,productbuild: \ + -s -k pwd \ + $RUNNER_TEMP/buildagent.keychain + + echo "Setting up notarytool" + xcrun notarytool store-credentials \ + --team-id $N1 \ + --apple-id $N2 \ + --password $N3 \ + "$N4" + + - name: Run codesign against payload env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} - APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }} + A3: ${{ secrets.APPLE_APPLICATION_SIGNING_IDENTITY }} run: | - python .github\run_esrp_signing.py payload ` - $env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE ` - --params 'Hardening' '--options=runtime' - - - name: Unzip signed payload - shell: pwsh - run: | - Expand-Archive signed/payload.zip -DestinationPath signed - Remove-Item signed/payload.zip - - - name: Upload signed payload - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.runtime }}-payload-sign - path: | - signed - - osx-pack: - name: Package macOS payload - runs-on: macos-latest - strategy: - matrix: - runtime: [ osx-x64, osx-arm64 ] - needs: osx-payload-sign - steps: - - uses: actions/checkout@v4 - - - name: Set version environment variable - run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - - name: Set up .NET - uses: actions/setup-dotnet@v3.2.0 - with: - dotnet-version: 7.0.x - - - name: Download signed payload - uses: actions/download-artifact@v3 - with: - name: ${{ matrix.runtime }}-payload-sign + ./src/osx/Installer.Mac/codesign.sh "payload" "$A3" \ + "$GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml" - name: Create component package run: | - src/osx/Installer.Mac/pack.sh --payload=payload \ - --version=$VERSION \ - --output=components/com.microsoft.gitcredentialmanager.component.pkg - - - name: Create product archive - run: | - src/osx/Installer.Mac/dist.sh --package-path=components \ - --version=$VERSION --runtime=${{ matrix.runtime }} \ - --output=pkg/gcm-${{ matrix.runtime }}-$VERSION.pkg || exit 1 + src/osx/Installer.Mac/pack.sh --payload="payload" \ + --version="${{ needs.prereqs.outputs.version }}" \ + --output="components/com.microsoft.gitcredentialmanager.component.pkg" - - name: Upload package - uses: actions/upload-artifact@v3 - with: - name: tmp.${{ matrix.runtime }}-pack - path: | - pkg - - osx-sign: - name: Sign and notarize macOS package - # ESRP service requires signing to run on Windows - runs-on: windows-latest - environment: release - strategy: - matrix: - runtime: [ osx-x64, osx-arm64 ] - needs: osx-pack - steps: - - uses: actions/checkout@v4 - - - name: Download unsigned package - uses: actions/download-artifact@v3 - with: - name: tmp.${{ matrix.runtime }}-pack - path: pkg - - - name: Zip unsigned package - shell: pwsh - run: | - Compress-Archive -Path pkg/*.pkg pkg/gcm-pkg.zip - cd pkg - Get-ChildItem -Exclude gcm-pkg.zip | Remove-Item -Recurse -Force - - - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Set up ESRP client - shell: pwsh + - name: Create and sign product archive env: - AZURE_VAULT: ${{ secrets.AZURE_VAULT }} - AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} - REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} + I3: ${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }} run: | - .github\set_up_esrp.ps1 - - - name: Sign package - shell: pwsh - env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} - APPLE_SIGNING_OP_CODE: ${{ secrets.APPLE_SIGNING_OPERATION_CODE }} - run: | - python .github\run_esrp_signing.py pkg $env:APPLE_KEY_CODE $env:APPLE_SIGNING_OP_CODE - - - name: Unzip signed package - shell: pwsh - run: | - mkdir unsigned - Expand-Archive -LiteralPath signed\gcm-pkg.zip -DestinationPath .\unsigned -Force - Remove-Item signed\gcm-pkg.zip -Force + src/osx/Installer.Mac/dist.sh --package-path=components \ + --version="${{ needs.prereqs.outputs.version }}" \ + --runtime="${{ matrix.runtime }}" \ + --output="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \ + --identity="$I3" || exit 1 - - name: Notarize signed package - shell: pwsh + - name: Notarize product archive env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - APPLE_KEY_CODE: ${{ secrets.APPLE_KEY_CODE }} - APPLE_NOTARIZATION_OP_CODE: ${{ secrets.APPLE_NOTARIZATION_OPERATION_CODE }} + N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }} run: | - python .github\run_esrp_signing.py unsigned $env:APPLE_KEY_CODE $env:APPLE_NOTARIZATION_OP_CODE --params 'BundleId' 'com.microsoft.gitcredentialmanager' + src/osx/Installer.Mac/notarize.sh \ + --package="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \ + --keychain-profile="$N4" - - name: Publish signed package + - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: ${{ matrix.runtime }}-sign - path: signed/*.pkg + name: macos-${{ matrix.runtime }}-artifacts + path: | + ./pkg/* + ./symbols/* + ./payload/* # ================================ # Windows @@ -624,7 +507,7 @@ jobs: command: git-credential-manager description: linux - os: macos-latest - artifact: osx-x64-sign + artifact: macos-osx-x64-artifacts command: git-credential-manager description: osx-x64 - os: windows-latest @@ -640,7 +523,7 @@ jobs: command: git-credential-manager description: dotnet-tool runs-on: ${{ matrix.component.os }} - needs: [ osx-sign, win-sign, create-linux-artifacts, dotnet-tool-sign ] + needs: [ create-macos-artifacts, win-sign, create-linux-artifacts, dotnet-tool-sign ] steps: - uses: actions/checkout@v4 @@ -670,7 +553,7 @@ jobs: if: contains(matrix.component.description, 'linux') run: | # Ensure we find only the source tarball, not the symbols - tarpath=$(find ./tar -name '*[[:digit:]].tar.gz') + tarpath=$(find . -name '*[[:digit:]].tar.gz') tar -xvf $tarpath -C /usr/local/bin "${{ matrix.component.command }}" configure @@ -678,7 +561,7 @@ jobs: if: contains(matrix.component.description, 'osx-x64') run: | # Only validate x64, given arm64 agents are not available - pkgpath=$(find ./*.pkg) + pkgpath=$(find ./pkg/*.pkg) sudo installer -pkg $pkgpath -target / - name: Install .NET tool @@ -716,13 +599,14 @@ jobs: - name: Archive macOS payload and symbols run: | + version="${{ needs.prereqs.outputs.version }}" mkdir osx-payload-and-symbols - tar -C osx-x64-payload-sign -czf osx-payload-and-symbols/gcm-osx-x64-$VERSION.tar.gz . - tar -C tmp.osx-x64-build/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$VERSION-symbols.tar.gz . + tar -C macos-osx-x64-artifacts/payload -czf osx-payload-and-symbols/gcm-osx-x64-$version.tar.gz . + tar -C macos-osx-x64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$version-symbols.tar.gz . - tar -C osx-arm64-payload-sign -czf osx-payload-and-symbols/gcm-osx-arm64-$VERSION.tar.gz . - tar -C tmp.osx-arm64-build/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$VERSION-symbols.tar.gz . + tar -C macos-osx-arm64-artifacts -czf osx-payload-and-symbols/gcm-osx-arm64-$version.tar.gz . + tar -C macos-osx-arm64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$version-symbols.tar.gz . - name: Archive Windows payload and symbols run: | @@ -780,8 +664,8 @@ jobs: uploadDirectoryToRelease('win-x86-payload-and-symbols'), // Upload macOS artifacts - uploadDirectoryToRelease('osx-x64-sign'), - uploadDirectoryToRelease('osx-arm64-sign'), + uploadDirectoryToRelease('macos-osx-x64-artifacts/pkg'), + uploadDirectoryToRelease('macos-osx-arm64-artifacts/pkg'), uploadDirectoryToRelease('osx-payload-and-symbols'), // Upload Linux artifacts diff --git a/.github/run_developer_signing.sh b/src/osx/Installer.Mac/codesign.sh similarity index 90% rename from .github/run_developer_signing.sh rename to src/osx/Installer.Mac/codesign.sh index 8b3de88a3..d66c8acd9 100755 --- a/.github/run_developer_signing.sh +++ b/src/osx/Installer.Mac/codesign.sh @@ -26,9 +26,9 @@ for f in * do macho=$(file --mime $f | grep mach) # Runtime sign dylibs and Mach-O binaries - if [[ $f == *.dylib ]] || [ ! -z "$macho" ]; - then - echo "Runtime Signing $f" + if [[ $f == *.dylib ]] || [ ! -z "$macho" ]; + then + echo "Runtime Signing $f" codesign -s "$DEVELOPER_ID" $f --timestamp --force --options=runtime --entitlements $ENTITLEMENTS_FILE elif [ -d "$f" ]; then @@ -39,8 +39,8 @@ do codesign -s "$DEVELOPER_ID" $i --timestamp --force done cd .. - else + else echo "Signing $f" codesign -s "$DEVELOPER_ID" $f --timestamp --force fi -done \ No newline at end of file +done diff --git a/src/osx/Installer.Mac/dist.sh b/src/osx/Installer.Mac/dist.sh index c60d0767b..f26761e26 100755 --- a/src/osx/Installer.Mac/dist.sh +++ b/src/osx/Installer.Mac/dist.sh @@ -35,6 +35,10 @@ case "$i" in RUNTIME="${i#*=}" shift ;; + --identity=*) + IDENTITY="${i#*=}" + shift + ;; *) # unknown option ;; @@ -93,6 +97,7 @@ echo "Building product package..." --distribution "$DISTPATH" \ --identifier "$IDENTIFIER" \ --version "$VERSION" \ + ${IDENTITY:+"--sign"} ${IDENTITY:+"$IDENTITY"} \ "$DISTOUT" || exit 1 echo "Product build complete." diff --git a/src/osx/Installer.Mac/notarize.sh b/src/osx/Installer.Mac/notarize.sh new file mode 100755 index 000000000..9315d688a --- /dev/null +++ b/src/osx/Installer.Mac/notarize.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +for i in "$@" +do +case "$i" in + --package=*) + PACKAGE="${i#*=}" + shift # past argument=value + ;; + --keychain-profile=*) + KEYCHAIN_PROFILE="${i#*=}" + shift # past argument=value + ;; + *) + die "unknown option '$i'" + ;; +esac +done + +if [ -z "$PACKAGE" ]; then + echo "error: missing package argument" + exit 1 +elif [ -z "$KEYCHAIN_PROFILE" ]; then + echo "error: missing keychain profile argument" + exit 1 +fi + +# Exit as soon as any line fails +set -e + +# Send the notarization request +xcrun notarytool submit -v "$PACKAGE" -p "$KEYCHAIN_PROFILE" --wait + +# Staple the notarization ticket (to allow offline installation) +xcrun stapler staple -v "$PACKAGE" From 4d3a8f134262cea23fe9c8178f70f6a48e77cd53 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 10 Oct 2023 17:47:26 -0600 Subject: [PATCH 34/45] release: update Windows to use Azure Code Signing Update Windows component of release workflow to use Azure Code Signing. --- .github/workflows/release.yml | 140 ++++++++++++++++------------------ 1 file changed, 65 insertions(+), 75 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e873dc41c..240394eaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,12 +139,13 @@ jobs: ./payload/* # ================================ -# Windows +# Windows # ================================ - win-sign: - name: Build and Sign Windows + create-windows-artifacts: + name: Create Windows Artifacts runs-on: windows-latest environment: release + needs: prereqs steps: - uses: actions/checkout@v4 @@ -153,9 +154,6 @@ jobs: with: dotnet-version: 7.0.x - - name: Install dependencies - run: dotnet restore - - name: Build run: | dotnet build --configuration=WindowsRelease @@ -165,80 +163,66 @@ jobs: dotnet test --configuration=WindowsRelease - name: Lay out Windows payload and symbols - shell: pwsh run: | - cd src/windows/Installer.Windows/ - ./layout.ps1 -Configuration WindowsRelease -Output payload -SymbolOutput symbols - mkdir unsigned-payload - Get-ChildItem -Path payload/* -Include *.exe, *.dll | Move-Item -Destination unsigned-payload + cd $env:GITHUB_WORKSPACE\src\windows\Installer.Windows\ + ./layout.ps1 -Configuration WindowsRelease ` + -Output $env:GITHUB_WORKSPACE\payload ` + -SymbolOutput $env:GITHUB_WORKSPACE\symbols - - uses: azure/login@v1 + - name: Log into Azure + uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Set up ESRP client - shell: pwsh - env: - AZURE_VAULT: ${{ secrets.AZURE_VAULT }} - AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} - REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} - run: | - .github\set_up_esrp.ps1 + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Run ESRP client for unsigned payload - shell: pwsh - env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }} - WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }} - run: | - python .github\run_esrp_signing.py ` - src/windows/Installer.Windows/unsigned-payload ` - $env:WINDOWS_KEY_CODE $env:WINDOWS_OP_CODE ` - --params 'OpusName' 'Microsoft' ` - 'OpusInfo' 'http://www.microsoft.com' ` - 'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' ` - 'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256' - - - name: Lay out signed payload - shell: pwsh - run: | - mkdir signed-payload - Move-Item -Path signed/* -Destination signed-payload - # ESRP will not sign the *.exe.config or NOTICE files, but they are needed to build the installers. - # Due to this, we copy them after signing. - Get-ChildItem -Path src/windows/Installer.Windows/payload/* -Include *.exe.config, NOTICE | Move-Item -Destination signed-payload - Remove-Item signed -Recurse -Force + - name: Sign payload files with Azure Code Signing + uses: azure/azure-code-signing-action@v0.2.21 + with: + endpoint: https://wus2.codesigning.azure.net/ + code-signing-account-name: git-fundamentals-signing + certificate-profile-name: git-fundamentals-windows-signing + files-folder: ${{ github.workspace }}\payload + files-folder-filter: exe,dll + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + # The Azure Code Signing action overrides the .NET version, so we reset it. + - name: Set up .NET + uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: 7.0.x - name: Build with signed payload - shell: pwsh run: | - dotnet build src/windows/Installer.Windows /p:PayloadPath=$env:GITHUB_WORKSPACE/signed-payload /p:NoLayout=true --configuration=WindowsRelease + dotnet build $env:GITHUB_WORKSPACE\src\windows\Installer.Windows ` + /p:PayloadPath=$env:GITHUB_WORKSPACE\payload /p:NoLayout=true ` + --configuration=WindowsRelease + mkdir installers + Move-Item -Path .\out\windows\Installer.Windows\bin\Release\net472\*.exe ` + -Destination $env:GITHUB_WORKSPACE\installers + + - name: Sign installers with Azure Code Signing + uses: azure/azure-code-signing-action@v0.2.21 + with: + endpoint: https://wus2.codesigning.azure.net/ + code-signing-account-name: git-fundamentals-signing + certificate-profile-name: git-fundamentals-windows-signing + files-folder: ${{ github.workspace }}\installers + files-folder-filter: exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 - - name: Run ESRP client for installers - shell: pwsh - env: - AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} - WINDOWS_KEY_CODE: ${{ secrets.WINDOWS_KEY_CODE }} - WINDOWS_OP_CODE: ${{ secrets.WINDOWS_OPERATION_CODE }} - run: | - python .github\run_esrp_signing.py ` - .\out\windows\Installer.Windows\bin\WindowsRelease\net472 ` - $env:WINDOWS_KEY_CODE ` - $env:WINDOWS_OP_CODE ` - --params 'OpusName' 'Microsoft' ` - 'OpusInfo' 'http://www.microsoft.com' ` - 'FileDigest' '/fd "SHA256"' 'PageHash' '/NPH' ` - 'TimeStamp' '/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256' - - - name: Publish final artifacts + - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: win-sign + name: windows-artifacts path: | - signed - signed-payload - src/windows/Installer.Windows/symbols + payload + installers + symbols # ================================ # Linux @@ -511,7 +495,7 @@ jobs: command: git-credential-manager description: osx-x64 - os: windows-latest - artifact: win-sign + artifact: windows-artifacts # Even when a standalone GCM version is installed, GitHub actions # runners still only recognize the version bundled with Git for # Windows due to its placement on the PATH. For this reason, we use @@ -523,10 +507,15 @@ jobs: command: git-credential-manager description: dotnet-tool runs-on: ${{ matrix.component.os }} - needs: [ create-macos-artifacts, win-sign, create-linux-artifacts, dotnet-tool-sign ] + needs: [ create-macos-artifacts, create-windows-artifacts, create-linux-artifacts, dotnet-tool-sign ] steps: - uses: actions/checkout@v4 + - name: Set up .NET + uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: 7.0.x + - name: Download artifacts uses: actions/download-artifact@v3 with: @@ -536,7 +525,7 @@ jobs: if: contains(matrix.component.description, 'windows') shell: pwsh run: | - $exePaths = Get-ChildItem -Path ./signed/*.exe | %{$_.FullName} + $exePaths = Get-ChildItem -Path ./installers/*.exe | %{$_.FullName} foreach ($exePath in $exePaths) { Start-Process -Wait -FilePath "$exePath" -ArgumentList "/SILENT /VERYSILENT /NORESTART" @@ -610,9 +599,10 @@ jobs: - name: Archive Windows payload and symbols run: | + version="${{ needs.prereqs.outputs.version }}" mkdir win-x86-payload-and-symbols - zip -jr win-x86-payload-and-symbols/gcm-win-x86-$VERSION.zip win-sign/signed-payload - zip -jr win-x86-payload-and-symbols/gcm-win-x86-$VERSION-symbols.zip win-sign/src/windows/Installer.Windows/symbols + zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version.zip windows-artifacts/payload + zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version-symbols.zip windows-artifacts/symbols - uses: actions/github-script@v6 with: @@ -660,7 +650,7 @@ jobs: await Promise.all([ // Upload Windows artifacts - uploadDirectoryToRelease('win-sign/signed'), + uploadDirectoryToRelease('windows-artifacts/installers'), uploadDirectoryToRelease('win-x86-payload-and-symbols'), // Upload macOS artifacts From 961a2131b16f96687d9290c12c1b42c731ea48e8 Mon Sep 17 00:00:00 2001 From: Lessley Date: Fri, 6 Oct 2023 20:58:08 -0600 Subject: [PATCH 35/45] release: update .NET tool azure connection/variables Update the .NET tool release workflow to use the workflow's Federated credential for Azure access. Additionally, update ESRP setup to use secrets instead of storage account/resource names. --- .github/set_up_esrp.ps1 | 2 +- .github/workflows/release.yml | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/set_up_esrp.ps1 b/.github/set_up_esrp.ps1 index ca56266e3..abe9183e0 100644 --- a/.github/set_up_esrp.ps1 +++ b/.github/set_up_esrp.ps1 @@ -1,5 +1,5 @@ # Install ESRP client -az storage blob download --file esrp.zip --auth-mode login --account-name esrpsigningstorage --container signing-resources --name microsoft.esrpclient.1.2.76.nupkg +az storage blob download --file esrp.zip --auth-mode login --account-name $env:AZURE_STORAGE_ACCOUNT --container $env:AZURE_STORAGE_CONTAINER --name $env:ESRP_TOOL Expand-Archive -Path esrp.zip -DestinationPath .\esrp # Install certificates diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 240394eaf..28d8ec115 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -309,6 +309,7 @@ jobs: dotnet-tool-build: name: Build .NET tool runs-on: ubuntu-latest + needs: prereqs steps: - uses: actions/checkout@v4 @@ -349,14 +350,20 @@ jobs: cd payload Get-ChildItem -Exclude payload.zip | Remove-Item -Recurse -Force - - uses: azure/login@v1 + - name: Log into Azure + uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Set up ESRP client shell: pwsh env: AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} + AZURE_STORAGE_CONTAINER: ${{ secrets.AZURE_STORAGE_CONTAINER }} + ESRP_TOOL: ${{ secrets.ESRP_TOOL }} AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} run: | @@ -390,15 +397,10 @@ jobs: dotnet-tool-pack: name: Package .NET tool runs-on: ubuntu-latest - needs: dotnet-tool-payload-sign + needs: [ prereqs, dotnet-tool-payload-sign ] steps: - uses: actions/checkout@v4 - - name: Set version environment variable - run: echo "VERSION=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_ENV - - - uses: actions/checkout@v4 - - name: Download signed payload uses: actions/download-artifact@v3 with: @@ -413,7 +415,8 @@ jobs: - name: Package tool run: | src/shared/DotnetTool/pack.sh --configuration=Release \ - --version=$VERSION --publish-dir=$(pwd)/signed + --version="${{ needs.prereqs.outputs.version }}" \ + --publish-dir=$(pwd)/signed - name: Upload unsigned package uses: actions/upload-artifact@v3 @@ -444,14 +447,20 @@ jobs: cd nupkg Get-ChildItem -Exclude gcm-nupkg.zip | Remove-Item -Recurse -Force - - uses: azure/login@v1 + - name: Log into Azure + uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Set up ESRP client shell: pwsh env: AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} + AZURE_STORAGE_CONTAINER: ${{ secrets.AZURE_STORAGE_CONTAINER }} + ESRP_TOOL: ${{ secrets.ESRP_TOOL }} AUTH_CERT: ${{ secrets.AZURE_VAULT_AUTH_CERT_NAME }} REQUEST_SIGNING_CERT: ${{ secrets.AZURE_VAULT_REQUEST_SIGNING_CERT_NAME }} run: | From 8f93d56c2d4c7064b778d56a3a76932c62a4b948 Mon Sep 17 00:00:00 2001 From: Lessley Date: Mon, 16 Oct 2023 15:50:28 -0600 Subject: [PATCH 36/45] release: validate linux signatures Upload GCM's public key as a release asset. Add instructions for users to import this key and use it to validate the latest Debian package and tarball. --- .github/workflows/release.yml | 17 +++++++ docs/install.md | 12 ++++- docs/linux-validate-gpg.md | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 docs/linux-validate-gpg.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28d8ec115..82ec3b1b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -582,6 +582,9 @@ jobs: create-github-release: name: Publish GitHub draft release runs-on: ubuntu-latest + env: + AZURE_VAULT: ${{ secrets.AZURE_VAULT }} + GPG_PUBLIC_KEY_SECRET_NAME: ${{ secrets.GPG_PUBLIC_KEY_SECRET_NAME }} environment: release needs: [ prereqs, validate ] steps: @@ -613,6 +616,20 @@ jobs: zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version.zip windows-artifacts/payload zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version-symbols.zip windows-artifacts/symbols + - name: Log into Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Download GPG public key signature file + run: | + az keyvault secret show --name "$GPG_PUBLIC_KEY_SECRET_NAME" \ + --vault-name "$AZURE_VAULT" --query "value" \ + | sed -e 's/^"//' -e 's/"$//' | base64 -d >gcm-public.asc + mv gcm-public.asc linux-artifacts + - uses: actions/github-script@v6 with: script: | diff --git a/docs/install.md b/docs/install.md index b4989ba13..4268858bb 100644 --- a/docs/install.md +++ b/docs/install.md @@ -72,7 +72,7 @@ installation method. #### Install -Download the latest [.deb package][latest-release], and run the following: +Download the latest [.deb package][latest-release]*, and run the following: ```shell sudo dpkg -i @@ -86,13 +86,16 @@ git-credential-manager unconfigure sudo dpkg -r gcm ``` +*If you'd like to validate the package's signature after downloading, check out +the instructions [here][linux-validate-gpg-debian]. + --- ### Tarball #### Install -Download the latest [tarball][latest-release], and run the following: +Download the latest [tarball][latest-release]*, and run the following: ```shell tar -xvf -C /usr/local/bin @@ -106,6 +109,9 @@ git-credential-manager unconfigure rm $(command -v git-credential-manager) ``` +*If you would like to validate the tarball's signature after downloading, check +out the instructions [here][linux-validate-gpg-tarball]. + --- ### Install from source helper script @@ -238,4 +244,6 @@ dotnet tool uninstall -g git-credential-manager [git-for-windows-screenshot]: https://user-images.githubusercontent.com/5658207/140082529-1ac133c1-0922-4a24-af03-067e27b3988b.png [latest-release]: https://github.com/git-ecosystem/git-credential-manager/releases/latest [linux-uninstall]: linux-fromsrc-uninstall.md +[linux-validate-gpg-debian]: ./linux-validate-gpg.md#debian-package +[linux-validate-gpg-tarball]: ./linux-validate-gpg.md#tarball [ms-wsl]: https://aka.ms/wsl# diff --git a/docs/linux-validate-gpg.md b/docs/linux-validate-gpg.md new file mode 100644 index 000000000..49150c1e5 --- /dev/null +++ b/docs/linux-validate-gpg.md @@ -0,0 +1,85 @@ +# Validating GCM's GPG signature + +Follow the below instructions to import GCM's public key and use it to validate +the latest Debian package and/or tarball signature. + +## Debian package + +```shell +# Install needed packages +apt-get install -y curl debsig-verify + +# Download public key signature file +curl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \ +| grep -E 'browser_download_url.*gcm-public.asc' \ +| cut -d : -f 2,3 \ +| tr -d \" \ +| xargs -I 'url' curl -L -o gcm-public.asc 'url' + +# De-armor public key signature file +gpg --output gcm-public.gpg --dearmor gcm-public.asc + +# Note that the fingerprint of this key is "3C853823978B07FA", which you can +# determine by running: +gpg --show-keys gcm-public.asc | head -n 2 | tail -n 1 | tail -c 17 + +# Copy de-armored public key to debsig keyring folder +mkdir /usr/share/debsig/keyrings/3C853823978B07FA +mv gcm-public.gpg /usr/share/debsig/keyrings/3C853823978B07FA/ + +# Create an appropriate policy file +mkdir /etc/debsig/policies/3C853823978B07FA +cat > /etc/debsig/policies/3C853823978B07FA/generic.pol << EOL + + + + + + + + + + + + + + + +EOL + +# Download Debian package +curl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \ +| grep "browser_download_url.*deb" \ +| cut -d : -f 2,3 \ +| tr -d \" \ +| xargs -I 'url' curl -L -o gcm.deb 'url' + +# Verify +debsig-verify gcm.deb +``` + +## Tarball +```shell +# Download the public key signature file +curl -s https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest \ +| grep -E 'browser_download_url.*gcm-public.asc' \ +| cut -d : -f 2,3 \ +| tr -d \" \ +| xargs -I 'url' curl -L -o gcm-public.asc 'url' + +# Import the public key +gpg --import gcm-public.asc + +# Download the tarball and its signature file +curl -s https://api.github.com/repos/ldennington/git-credential-manager/releases/latest \ +| grep -E 'browser_download_url.*gcm-linux.*[0-9].[0-9].[0-9].tar.gz' \ +| cut -d : -f 2,3 \ +| tr -d \" \ +| xargs -I 'url' curl -LO 'url' + +# Trust the public key +echo -e "5\ny\n" | gpg --command-fd 0 --expert --edit-key 3C853823978B07FA trust + +# Verify the signature +gpg --verify gcm-linux_amd64*.tar.gz.asc gcm-linux*.tar.gz +``` From 12ab89ba3e850f12ae12a145cfb1240b97d9380c Mon Sep 17 00:00:00 2001 From: M Hickford Date: Thu, 19 Oct 2023 18:33:54 +0100 Subject: [PATCH 37/45] recognise BitBucket hosts by WWW-Authenticate header Tested with https://bitbucket.hl7.org --- .../BitbucketHostProviderTest.cs | 14 ++++++++++++++ .../Atlassian.Bitbucket/BitbucketHostProvider.cs | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs index 4e277d0f3..36b116615 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs @@ -55,6 +55,20 @@ public void BitbucketHostProvider_IsSupported(string protocol, string host, bool Assert.Equal(expected, provider.IsSupported(input)); } + [Theory] + [InlineData("Basic realm=\"Atlassian Bitbucket\"", true)] + [InlineData("Basic realm=\"GitSponge\"", false)] + public void BitbucketHostProvider_IsSupported_WWWAuth(string wwwauth, bool expected) + { + var input = new InputArguments(new Dictionary + { + ["wwwauth"] = wwwauth, + }); + + var provider = new BitbucketHostProvider(new TestCommandContext()); + Assert.Equal(expected, provider.IsSupported(input)); + } + [Fact] public void BitbucketHostProvider_IsSupported_FailsForNullInput() { diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index f3f653f01..35472682c 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Atlassian.Bitbucket.Cloud; @@ -43,6 +44,11 @@ public bool IsSupported(InputArguments input) return false; } + if (input.WwwAuth.Any(x => x.Contains("realm=\"Atlassian Bitbucket\"", StringComparison.InvariantCultureIgnoreCase))) + { + return true; + } + // Split port number and hostname from host input argument if (!input.TryGetHostAndPort(out string hostName, out _)) { From c512d1b0e7320ab2fc7cfac6fe1eafb90af0b273 Mon Sep 17 00:00:00 2001 From: M Hickford Date: Thu, 19 Oct 2023 21:51:41 +0100 Subject: [PATCH 38/45] use universal Gitea OAuth configuration https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications --- .../Core.Tests/GenericOAuthConfigTests.cs | 50 ++++++++++++++++++- src/shared/Core/GenericHostProvider.cs | 2 +- src/shared/Core/GenericOAuthConfig.cs | 44 ++++++++++++---- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/shared/Core.Tests/GenericOAuthConfigTests.cs b/src/shared/Core.Tests/GenericOAuthConfigTests.cs index 08dfacab4..a0cb5f731 100644 --- a/src/shared/Core.Tests/GenericOAuthConfigTests.cs +++ b/src/shared/Core.Tests/GenericOAuthConfigTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GitCredentialManager.Tests.Objects; using Xunit; @@ -9,7 +10,9 @@ public class GenericOAuthConfigTests [Fact] public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue() { - var remoteUri = new Uri("https://example.com"); + var protocol = "https"; + var host = "example.com"; + var remoteUri = new Uri($"{protocol}://{host}"); const string expectedClientId = "115845b0-77f8-4c06-a3dc-7d277381fad1"; const string expectedClientSecret = "4D35385D9F24"; const string expectedUserName = "TEST_USER"; @@ -44,7 +47,12 @@ public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue() RemoteUri = remoteUri }; - bool result = GenericOAuthConfig.TryGet(trace, settings, remoteUri, out GenericOAuthConfig config); + var input = new InputArguments(new Dictionary { + {"protocol", protocol}, + {"host", host}, + }); + + bool result = GenericOAuthConfig.TryGet(trace, settings, input, out GenericOAuthConfig config); Assert.True(result); Assert.Equal(expectedClientId, config.ClientId); @@ -57,5 +65,43 @@ public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue() Assert.Equal(expectedUserName, config.DefaultUserName); Assert.True(config.UseAuthHeader); } + + [Fact] + public void GenericOAuthConfig_TryGet_Gitea() + { + var protocol = "https"; + var host = "example.com"; + var remoteUri = new Uri($"{protocol}://{host}"); + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications + const string expectedClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662"; + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints + const string authzEndpoint = "/login/oauth/authorize"; + const string tokenEndpoint = "/login/oauth/access_token"; + string[] expectedScopes = Array.Empty(); + var expectedRedirectUri = new Uri("http://127.0.0.1"); + var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint); + var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint); + + var trace = new NullTrace(); + var settings = new TestSettings + { + RemoteUri = remoteUri + }; + + var input = new InputArguments(new Dictionary { + {"protocol", protocol}, + {"host", host}, + {"wwwauth", "Basic realm=\"Gitea\""} + }); + + bool result = GenericOAuthConfig.TryGet(trace, settings, input, out GenericOAuthConfig config); + + Assert.True(result); + Assert.Equal(expectedClientId, config.ClientId); + Assert.Equal(expectedRedirectUri, config.RedirectUri); + Assert.Equal(expectedScopes, config.Scopes); + Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint); + Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint); + } } } diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index cdba0ba8a..447e465d5 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -63,7 +63,7 @@ public override async Task GenerateCredentialAsync(InputArguments i // Cannot check WIA or OAuth support for non-HTTP based protocols } // Check for an OAuth configuration for this remote - else if (GenericOAuthConfig.TryGet(Context.Trace, Context.Settings, uri, out GenericOAuthConfig oauthConfig)) + else if (GenericOAuthConfig.TryGet(Context.Trace, Context.Settings, input, out GenericOAuthConfig oauthConfig)) { Context.Trace.WriteLine($"Found generic OAuth configuration for '{uri}':"); Context.Trace.WriteLine($"\tAuthzEndpoint = {oauthConfig.Endpoints.AuthorizationEndpoint}"); diff --git a/src/shared/Core/GenericOAuthConfig.cs b/src/shared/Core/GenericOAuthConfig.cs index 0e2a74b75..09d21436f 100644 --- a/src/shared/Core/GenericOAuthConfig.cs +++ b/src/shared/Core/GenericOAuthConfig.cs @@ -1,32 +1,56 @@ using System; +using System.Collections.Generic; +using System.Linq; using GitCredentialManager.Authentication.OAuth; namespace GitCredentialManager { public class GenericOAuthConfig { - public static bool TryGet(ITrace trace, ISettings settings, Uri remoteUri, out GenericOAuthConfig config) + public static bool TryGet(ITrace trace, ISettings settings, InputArguments input, out GenericOAuthConfig config) { config = new GenericOAuthConfig(); + Uri authzEndpointUri = null; + Uri tokenEndpointUri = null; + var remoteUri = input.GetRemoteUri(); - if (!settings.TryGetSetting( + if (input.WwwAuth.Any(x => x.Contains("Basic realm=\"Gitea\""))) + { + trace.WriteLine($"Using universal Gitea OAuth configuration"); + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications + config.ClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662"; + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints + authzEndpointUri = new Uri(remoteUri, "/login/oauth/authorize"); + tokenEndpointUri = new Uri(remoteUri, "/login/oauth/access_token"); + config.RedirectUri = new Uri("http://127.0.0.1"); + } + + if (settings.TryGetSetting( Constants.EnvironmentVariables.OAuthAuthzEndpoint, Constants.GitConfiguration.Credential.SectionName, Constants.GitConfiguration.Credential.OAuthAuthzEndpoint, - out string authzEndpoint) || - !Uri.TryCreate(remoteUri, authzEndpoint, out Uri authzEndpointUri)) + out string authzEndpoint)) + { + Uri.TryCreate(remoteUri, authzEndpoint, out authzEndpointUri); + } + + if (authzEndpointUri == null) { trace.WriteLine($"Invalid OAuth configuration - missing/invalid authorize endpoint: {authzEndpoint}"); config = null; return false; } - if (!settings.TryGetSetting( + if (settings.TryGetSetting( Constants.EnvironmentVariables.OAuthTokenEndpoint, Constants.GitConfiguration.Credential.SectionName, Constants.GitConfiguration.Credential.OAuthTokenEndpoint, - out string tokenEndpoint) || - !Uri.TryCreate(remoteUri, tokenEndpoint, out Uri tokenEndpointUri)) + out string tokenEndpoint)) + { + Uri.TryCreate(remoteUri, tokenEndpoint, out tokenEndpointUri); + } + + if (tokenEndpointUri == null) { trace.WriteLine($"Invalid OAuth configuration - missing/invalid token endpoint: {tokenEndpoint}"); config = null; @@ -74,12 +98,12 @@ public static bool TryGet(ITrace trace, ISettings settings, Uri remoteUri, out G Constants.EnvironmentVariables.OAuthRedirectUri, Constants.GitConfiguration.Credential.SectionName, Constants.GitConfiguration.Credential.OAuthRedirectUri, - out string redirectUrl) && - Uri.TryCreate(redirectUrl, UriKind.Absolute, out Uri redirectUri)) + out string redirectUrl) && Uri.TryCreate(redirectUrl, UriKind.Absolute, out Uri redirectUri)) { config.RedirectUri = redirectUri; } - else + + if (config.RedirectUri == null) { trace.WriteLine($"Invalid OAuth configuration - missing/invalid redirect URI: {redirectUrl}"); config = null; From 46810dfd9ae67851241ab121b589a54e78e823e7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 28 Sep 2023 09:24:36 -0700 Subject: [PATCH 39/45] avalonia: introduce flag to switch to SW rendering Introduce a flag to switch Avalonia to use software rendering rather than hardware/GPU-based. There is an open Avalonia issue[1] on Windows when run on certain ARM64 GPUs. Until this is solved, introduce this workaround flag. [1]: https://github.com/AvaloniaUI/Avalonia/issues/10405 --- docs/configuration.md | 19 +++++++++++ docs/environment.md | 27 ++++++++++++++++ src/shared/Core/ApplicationBase.cs | 7 ++++ src/shared/Core/Constants.cs | 2 ++ src/shared/Core/Settings.cs | 16 ++++++++++ src/shared/Core/UI/AvaloniaUi.cs | 32 +++++++++++++++++-- .../Objects/TestSettings.cs | 2 ++ 7 files changed, 103 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 88a23c103..ac0658b46 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -233,6 +233,24 @@ Defaults to enabled. --- +### credential.guiSoftwareRendering + +Force the use of software rendering for GUI prompts. + +This is currently only applicable on Windows. + +#### Example + +```shell +git config --global credential.guiSoftwareRendering true +``` + +Defaults to false (use hardware acceleration where available). + +**Also see: [GCM_GUI_SOFTWARE_RENDERING][gcm-gui-software-rendering]** + +--- + ### credential.autoDetectTimeout Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -978,6 +996,7 @@ Defaults to disabled. [gcm-github-authmodes]: environment.md#GCM_GITHUB_AUTHMODES [gcm-gitlab-authmodes]:environment.md#GCM_GITLAB_AUTHMODES [gcm-gui-prompt]: environment.md#GCM_GUI_PROMPT +[gcm-gui-software-rendering]: environment.md#GCM_GUI_SOFTWARE_RENDERING [gcm-http-proxy]: environment.md#GCM_HTTP_PROXY-deprecated [gcm-interactive]: environment.md#GCM_INTERACTIVE [gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW diff --git a/docs/environment.md b/docs/environment.md index 666604422..3612c1d0f 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -272,6 +272,32 @@ Defaults to enabled. --- +### GCM_GUI_SOFTWARE_RENDERING + +Force the use of software rendering for GUI prompts. + +This is currently only applicable on Windows. + +#### Example + +##### Windows + +```batch +SET GCM_GUI_SOFTWARE_RENDERING=1 +``` + +##### macOS/Linux + +```bash +export GCM_GUI_SOFTWARE_RENDERING=1 +``` + +Defaults to false (use hardware acceleration where available). + +**Also see: [credential.guiSoftwareRendering][credential-guisoftwarerendering]** + +--- + ### GCM_AUTODETECT_TIMEOUT Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -1111,6 +1137,7 @@ Defaults to disabled. [credential-githubauthmodes]: configuration.md#credentialgitHubAuthModes [credential-gitlabauthmodes]: configuration.md#credentialgitLabAuthModes [credential-guiprompt]: configuration.md#credentialguiprompt +[credential-guisoftwarerendering]: configuration.md#credentialguisoftwarerendering [credential-httpproxy]: configuration.md#credentialhttpProxy-deprecated [credential-interactive]: configuration.md#credentialinteractive [credential-namespace]: configuration.md#credentialnamespace diff --git a/src/shared/Core/ApplicationBase.cs b/src/shared/Core/ApplicationBase.cs index 42f1390e9..5ff692a4d 100644 --- a/src/shared/Core/ApplicationBase.cs +++ b/src/shared/Core/ApplicationBase.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using GitCredentialManager.UI; namespace GitCredentialManager { @@ -74,6 +75,12 @@ public Task RunAsync(string[] args) Context.Trace.WriteLine("Tracing of secrets is enabled. Trace output may contain sensitive information."); } + // Set software rendering if defined in settings + if (Context.Settings.UseSoftwareRendering) + { + AvaloniaUi.Initialize(win32SoftwareRendering: true); + } + return RunInternalAsync(args); } diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 03f41647a..ac609adaa 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -118,6 +118,7 @@ public static class EnvironmentVariables public const string OAuthClientAuthHeader = "GCM_OAUTH_USE_CLIENT_AUTH_HEADER"; public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME"; public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS"; + public const string GcmGuiSoftwareRendering = "GCM_GUI_SOFTWARE_RENDERING"; } public static class Http @@ -160,6 +161,7 @@ public static class Credential public const string UiHelper = "uiHelper"; public const string DevUseLegacyUiHelpers = "devUseLegacyUiHelpers"; public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount"; + public const string GuiSoftwareRendering = "guiSoftwareRendering"; public const string OAuthAuthenticationModes = "oauthAuthModes"; public const string OAuthClientId = "oauthClientId"; diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 1e60793c1..ac92b23f8 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -184,6 +184,11 @@ public interface ISettings : IDisposable /// bool UseMsAuthDefaultAccount { get; } + /// + /// True if software rendering should be used for graphical user interfaces, false otherwise. + /// + bool UseSoftwareRendering { get; } + /// /// Get TRACE2 settings. /// @@ -559,6 +564,17 @@ public bool IsInteractionAllowed KnownGitCfg.Credential.Trace, out value) && !value.IsFalsey(); + public bool UseSoftwareRendering + { + get + { + return TryGetSetting(KnownEnvars.GcmGuiSoftwareRendering, + KnownGitCfg.Credential.SectionName, + KnownGitCfg.Credential.GuiSoftwareRendering, + out string str) && str.ToBooleanyOrDefault(false); + } + } + public Trace2Settings GetTrace2Settings() { var settings = new Trace2Settings(); diff --git a/src/shared/Core/UI/AvaloniaUi.cs b/src/shared/Core/UI/AvaloniaUi.cs index 477979ae6..0c2ca237e 100644 --- a/src/shared/Core/UI/AvaloniaUi.cs +++ b/src/shared/Core/UI/AvaloniaUi.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using GitCredentialManager.Interop.Windows.Native; using GitCredentialManager.UI.Controls; @@ -15,6 +14,24 @@ namespace GitCredentialManager.UI public static class AvaloniaUi { private static bool _isAppStarted; + private static bool _win32SoftwareRendering; + + /// + /// Configure the Avalonia application. + /// + /// True to enable software rendering on Windows, false otherwise. + /// + /// This must be invoked before the Avalonia application loop has started. + /// + public static void Initialize(bool win32SoftwareRendering) + { + if (_isAppStarted) + { + throw new InvalidOperationException("Setup must be called before the Avalonia application is started."); + } + + _win32SoftwareRendering = win32SoftwareRendering; + } public static Task ShowViewAsync(Func viewFunc, WindowViewModel viewModel, IntPtr parentHandle, CancellationToken ct) => ShowWindowAsync(() => new DialogWindow(viewFunc()), viewModel, parentHandle, ct); @@ -46,7 +63,18 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext, // This action only returns on our dispatcher shutdown. Dispatcher.MainThread.Post(appCancelToken => { - AppBuilder.Configure() + var appBuilder = AppBuilder.Configure(); + +#if NETFRAMEWORK + // Set custom rendering options and modes if required + if (PlatformUtils.IsWindows() && _win32SoftwareRendering) + { + appBuilder.With(new Win32PlatformOptions + { RenderingMode = new[] { Win32RenderingMode.Software } }); + } +#endif + + appBuilder #if NETFRAMEWORK .UseWin32() .UseSkia() diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index 265d411f0..f14bf6cc9 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -187,6 +187,8 @@ ProxyConfiguration ISettings.GetProxyConfiguration() bool ISettings.UseMsAuthDefaultAccount => UseMsAuthDefaultAccount; + bool ISettings.UseSoftwareRendering => false; + #endregion #region IDisposable From ee35d7bf763f96db1be078a63dec51d81a6c4c8f Mon Sep 17 00:00:00 2001 From: M Hickford Date: Fri, 20 Oct 2023 19:48:25 +0100 Subject: [PATCH 40/45] default OAuth redirect URI to http://127.0.0.1 --- docs/generic-oauth.md | 2 +- src/shared/Core/GenericOAuthConfig.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/generic-oauth.md b/docs/generic-oauth.md index 6620134fc..08b735ccb 100644 --- a/docs/generic-oauth.md +++ b/docs/generic-oauth.md @@ -40,7 +40,7 @@ following values in your Git configuration: - Client ID - Client Secret (optional) -- Redirect URL +- Redirect URL (optional, defaults to `http://127.0.0.1`) - Scopes (optional) - OAuth Endpoints - Authorization Endpoint diff --git a/src/shared/Core/GenericOAuthConfig.cs b/src/shared/Core/GenericOAuthConfig.cs index 0e2a74b75..1f8aa16fb 100644 --- a/src/shared/Core/GenericOAuthConfig.cs +++ b/src/shared/Core/GenericOAuthConfig.cs @@ -81,9 +81,7 @@ public static bool TryGet(ITrace trace, ISettings settings, Uri remoteUri, out G } else { - trace.WriteLine($"Invalid OAuth configuration - missing/invalid redirect URI: {redirectUrl}"); - config = null; - return false; + config.RedirectUri = new Uri("http://127.0.0.1"); } if (settings.TryGetSetting( From 66292a90f90492713f30e58e62fe5e614fdcadff Mon Sep 17 00:00:00 2001 From: M Hickford Date: Mon, 23 Oct 2023 21:53:37 +0100 Subject: [PATCH 41/45] Apply suggestions from code review Co-authored-by: Matthew John Cheetham --- .../Core.Tests/GenericOAuthConfigTests.cs | 12 ++++------- src/shared/Core/GenericOAuthConfig.cs | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/shared/Core.Tests/GenericOAuthConfigTests.cs b/src/shared/Core.Tests/GenericOAuthConfigTests.cs index a0cb5f731..b05ae2e8b 100644 --- a/src/shared/Core.Tests/GenericOAuthConfigTests.cs +++ b/src/shared/Core.Tests/GenericOAuthConfigTests.cs @@ -72,15 +72,11 @@ public void GenericOAuthConfig_TryGet_Gitea() var protocol = "https"; var host = "example.com"; var remoteUri = new Uri($"{protocol}://{host}"); - // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications - const string expectedClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662"; - // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints - const string authzEndpoint = "/login/oauth/authorize"; - const string tokenEndpoint = "/login/oauth/access_token"; + const string expectedClientId = GenericOAuthConfig.WellKnown.GiteaClientId; string[] expectedScopes = Array.Empty(); - var expectedRedirectUri = new Uri("http://127.0.0.1"); - var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint); - var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint); + var expectedRedirectUri = GenericOAuthConfig.WellKnown.LocalIPv4RedirectUri; + var expectedAuthzEndpoint = new Uri(remoteUri, GenericOAuthConfig.WellKnown.GiteaAuthzEndpoint); + var expectedTokenEndpoint = new Uri(remoteUri, GenericOAuthConfig.WellKnown.GiteaTokenEndpoint); var trace = new NullTrace(); var settings = new TestSettings diff --git a/src/shared/Core/GenericOAuthConfig.cs b/src/shared/Core/GenericOAuthConfig.cs index 09d21436f..fc0434b67 100644 --- a/src/shared/Core/GenericOAuthConfig.cs +++ b/src/shared/Core/GenericOAuthConfig.cs @@ -14,15 +14,14 @@ public static bool TryGet(ITrace trace, ISettings settings, InputArguments input Uri tokenEndpointUri = null; var remoteUri = input.GetRemoteUri(); - if (input.WwwAuth.Any(x => x.Contains("Basic realm=\"Gitea\""))) + if (input.WwwAuth.Any(x => x.Contains("Basic realm=\"Gitea\"", StringComparison.OrdinalIgnoreCase))) { trace.WriteLine($"Using universal Gitea OAuth configuration"); // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications - config.ClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662"; - // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints - authzEndpointUri = new Uri(remoteUri, "/login/oauth/authorize"); - tokenEndpointUri = new Uri(remoteUri, "/login/oauth/access_token"); - config.RedirectUri = new Uri("http://127.0.0.1"); + config.ClientId = WellKnown.GiteaClientId; + authzEndpointUri = new Uri(remoteUri, WellKnown.GiteaAuthzEndpoint); + tokenEndpointUri = new Uri(remoteUri, WellKnown.GiteaTokenEndpoint); + config.RedirectUri = WellKnown.LocalIPv4RedirectUri; } if (settings.TryGetSetting( @@ -158,5 +157,15 @@ public static bool TryGet(ITrace trace, ISettings settings, InputArguments input public string DefaultUserName { get; set; } public bool SupportsDeviceCode => Endpoints.DeviceAuthorizationEndpoint != null; + + public static class WellKnown + { + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#pre-configured-applications + public const string GiteaClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662"; + // https://docs.gitea.com/next/development/oauth2-provider?_highlight=oauth#endpoints + public const string GiteaAuthzEndpoint = "/login/oauth/authorize"; + public const string GiteaTokenEndpoint = "/login/oauth/access_token"; + public static Uri LocalIPv4RedirectUri = new Uri("http://127.0.0.1"); + } } } From 3183801eacdc6653e9046d9d2fbeb6c59efc8408 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 23 Oct 2023 12:16:12 -0700 Subject: [PATCH 42/45] settings: default SW rendering on Windows+ARM Default to software GUI rendering on Windows on ARM. Users can explicitly set the config to re-enable HW accelerated rendering if they wish. --- docs/configuration.md | 4 ++++ docs/environment.md | 4 ++++ src/shared/Core/PlatformUtils.cs | 16 ++++++++++++++++ src/shared/Core/Settings.cs | 7 ++++++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ac0658b46..2439c9297 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -247,6 +247,10 @@ git config --global credential.guiSoftwareRendering true Defaults to false (use hardware acceleration where available). +> [!NOTE] +> Windows on ARM devices defaults to using software rendering to work around a +> known Avalonia issue: + **Also see: [GCM_GUI_SOFTWARE_RENDERING][gcm-gui-software-rendering]** --- diff --git a/docs/environment.md b/docs/environment.md index 3612c1d0f..18f3f05fe 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -294,6 +294,10 @@ export GCM_GUI_SOFTWARE_RENDERING=1 Defaults to false (use hardware acceleration where available). +> [!NOTE] +> Windows on ARM devices defaults to using software rendering to work around a +> known Avalonia issue: + **Also see: [credential.guiSoftwareRendering][credential-guisoftwarerendering]** --- diff --git a/src/shared/Core/PlatformUtils.cs b/src/shared/Core/PlatformUtils.cs index 212c13219..ed0e2ec7d 100644 --- a/src/shared/Core/PlatformUtils.cs +++ b/src/shared/Core/PlatformUtils.cs @@ -53,6 +53,22 @@ public static bool IsDevBox() #endif } + /// + /// Returns true if the current process is running on an ARM processor. + /// + /// True if ARM(v6,hf) or ARM64, false otherwise + public static bool IsArm() + { + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.Arm: + case Architecture.Arm64: + return true; + default: + return false; + } + } + public static bool IsWindowsBrokerSupported() { if (!IsWindows()) diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index ac92b23f8..2aa71edf4 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -568,10 +568,15 @@ public bool UseSoftwareRendering { get { + // WORKAROUND: Some Windows ARM devices have a graphics driver issue that causes transparent windows + // when using hardware rendering. Until this is fixed, we will default to software rendering on these + // devices. Users can always override this setting back to HW-accelerated rendering if they wish. + bool defaultValue = PlatformUtils.IsWindows() && PlatformUtils.IsArm(); + return TryGetSetting(KnownEnvars.GcmGuiSoftwareRendering, KnownGitCfg.Credential.SectionName, KnownGitCfg.Credential.GuiSoftwareRendering, - out string str) && str.ToBooleanyOrDefault(false); + out string str) ? str.ToBooleanyOrDefault(defaultValue) : defaultValue; } } From 81211fba139cc30c6eac7368a7684bc8663c9cd0 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 25 Oct 2023 11:53:34 -0700 Subject: [PATCH 43/45] platformutils: drop unnecessary netfx #ifdefs Since we're targetting .NET Framework 4.7.2 there are some unnecessary conditional compliation `#if NETFRAMEWORK` blocks. Namely around CLR and runtime/OS detection. --- src/shared/Core/PlatformUtils.cs | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/shared/Core/PlatformUtils.cs b/src/shared/Core/PlatformUtils.cs index 212c13219..5d00ffcf6 100644 --- a/src/shared/Core/PlatformUtils.cs +++ b/src/shared/Core/PlatformUtils.cs @@ -19,7 +19,7 @@ public static PlatformInformation GetPlatformInformation(ITrace2 trace2) string osType = GetOSType(); string osVersion = GetOSVersion(trace2); string cpuArch = GetCpuArchitecture(); - string clrVersion = GetClrVersion(); + string clrVersion = RuntimeInformation.FrameworkDescription; return new PlatformInformation(osType, osVersion, cpuArch, clrVersion); } @@ -99,11 +99,7 @@ public static bool IsWindowsBrokerSupported() /// True if running on macOS, false otherwise. public static bool IsMacOS() { -#if NETFRAMEWORK - return Environment.OSVersion.Platform == PlatformID.MacOSX; -#else return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); -#endif } /// @@ -112,11 +108,7 @@ public static bool IsMacOS() /// True if running on Windows, false otherwise. public static bool IsWindows() { -#if NETFRAMEWORK - return Environment.OSVersion.Platform == PlatformID.Win32NT; -#else return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); -#endif } /// @@ -125,11 +117,7 @@ public static bool IsWindows() /// True if running on a Linux distribution, false otherwise. public static bool IsLinux() { -#if NETFRAMEWORK - return Environment.OSVersion.Platform == PlatformID.Unix; -#else return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); -#endif } /// @@ -459,9 +447,6 @@ string GetLinuxDistroVersion() private static string GetCpuArchitecture() { -#if NETFRAMEWORK - return Environment.Is64BitOperatingSystem ? "x86-64" : "x86"; -#else switch (RuntimeInformation.OSArchitecture) { case Architecture.Arm: @@ -475,16 +460,6 @@ private static string GetCpuArchitecture() default: return RuntimeInformation.OSArchitecture.ToString(); } -#endif - } - - private static string GetClrVersion() - { -#if NETFRAMEWORK - return $".NET Framework {Environment.Version}"; -#else - return RuntimeInformation.FrameworkDescription; -#endif } #endregion From d928878020eb55845755f2614122056ed5ef50a6 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 30 Oct 2023 11:38:42 -0700 Subject: [PATCH 44/45] avalonia: actually setup Avalonia before starting In removing the Avalonia setup workaround in #1445 we forget to replace the SetupWithLifetime call with just SetupWithoutStarting! --- src/shared/Core/UI/AvaloniaUi.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/Core/UI/AvaloniaUi.cs b/src/shared/Core/UI/AvaloniaUi.cs index 0c2ca237e..34021c595 100644 --- a/src/shared/Core/UI/AvaloniaUi.cs +++ b/src/shared/Core/UI/AvaloniaUi.cs @@ -81,7 +81,8 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext, #else .UsePlatformDetect() #endif - .LogToTrace(); + .LogToTrace() + .SetupWithoutStarting(); appInitialized.Set(); From 3a60ecf357daa6e82441d4d918a6ab03302e18ca Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 1 Nov 2023 09:00:10 -0700 Subject: [PATCH 45/45] version: bump to 2.4.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5b3c26b59..c2fa7a287 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.3.2.0 +2.4.0.0