diff --git a/README.md b/README.md index 92fd9411b..48b824f5a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ See detailed information [here](https://aka.ms/gcmcore-httpproxy). - [Command-line usage](docs/usage.md) - [Configuration options](docs/configuration.md) - [Environment variables](docs/environment.md) +- [Enterprise configuration](docs/enterprise-config.md) - [Network and HTTP configuration](docs/netconfig.md) - [Architectural overview](docs/architecture.md) - [Host provider specification](docs/hostprovider.md) diff --git a/docs/configuration.md b/docs/configuration.md index 48a025f40..1ea541542 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,12 +3,13 @@ [Git Credential Manager Core](usage.md) works out of the box for most users. Git Credential Manager Core (GCM Core) can be configured using Git's configuration files, and follows all of the same rules Git does when consuming the files. + Global configuration settings override system configuration settings, and local configuration settings override global settings; and because the configuration details exist within Git's configuration files you can use Git's `git config` utility to set, unset, and alter the setting values. All of GCM Core's configuration settings begin with the term `credential`. GCM Core honors several levels of settings, in addition to the standard local \> global \> system tiering Git uses. URL-specific settings or overrides can be applied to any value in the `credential` namespace with the syntax below. -Additionally, GCM Core respects several GCM-specific [environment variables](environment.md) **which take precedence over configuration options.** +Additionally, GCM Core respects several GCM-specific [environment variables](environment.md) **which take precedence over configuration options**. System administrators may also configure [default values](enterprise-config.md) for many settings used by GCM Core. GCM Core will only be used by Git if it is installed and configured. Use `git config --global credential.helper manager-core` to assign GCM Core as your credential helper. Use `git config credential.helper` to see the current configuration. diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md new file mode 100644 index 000000000..d6827d1d4 --- /dev/null +++ b/docs/enterprise-config.md @@ -0,0 +1,61 @@ +# Enterprise configuration defaults + +Git Credential Manager Core (GCM Core) can be configured using multiple +different mechanisms. In order of preference, those mechanisms are: + +1. [Environment variables](environment.md) +2. [Standard Git configuration files](configuration.md) + 1. Repository/local configuration (`.git/config`) + 2. User/global configuration (`$HOME/.gitconfig` or `%HOME%\.gitconfig`) + 3. Installation/system configuration (`etc/gitconfig`) +3. Enterprise system administrator defaults +4. Compiled default values + +This model largely matches what Git itself supports, namely environment +variables that take precedence over Git configuration files. + +The addition of the enterprise system administrator defaults enables those +administrators to configure many GCM settings using familiar MDM tooling, rather +than having to modify the Git installation configuration files. + +### User Freedom + +We believe the user should _always_ be at liberty to configure +Git and GCM exactly as they wish. By prefering environment variables and Git +configuration files over system admin values, these only act as _default values_ +that can always be overriden by the user in the usual ways. + +## Windows + +Default setting values come from the Windows Registry, specifically the +following keys: + +**32-bit Windows** + +```text +HKEY_LOCAL_MACHINE\SOFTWARE\GitCredentialManager\Configuration +``` + +**64-bit Windows** + +```text +HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GitCredentialManager\Configuration +``` + +> GCM Core is a 32-bit executable on Windows. When running on a 64-bit +installation of Windows registry access is transparently redirected to the +`WOW6432Node` node. + +By using the Windows Registry, system administrators can use Group Policy to +easily set defaults for GCM Core's settings. + +The names and possible values of all settings under this key are the same as +those of the [Git configuration](configuration.md) settings. + +The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` +(integer). + + +## macOS/Linux + +Default configuration setting stores has not been implemented. diff --git a/docs/environment.md b/docs/environment.md index 906390df5..ab9f6beeb 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -2,7 +2,7 @@ [Git Credential Manager Core](usage.md) works out of the box for most users. Configuration options are available to customize or tweak behavior. -Git Credential Manager Core (GCM Core) can be configured using environment variables. **Environment variables take precedence over [configuration](configuration.md) options.** +Git Credential Manager Core (GCM Core) can be configured using environment variables. **Environment variables take precedence over [configuration](configuration.md) options and enterprise system administrator [default values](enterprise-config.md)**. For the complete list of environment variables GCM Core understands, see the list below. diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs index f65060913..ab5940849 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. using System; using Xunit; +using Microsoft.Git.CredentialManager.Interop; using Microsoft.Git.CredentialManager.Interop.MacOS; +using Microsoft.Git.CredentialManager.Interop.MacOS.Native; namespace Microsoft.Git.CredentialManager.Tests.Interop.MacOS { @@ -10,7 +12,7 @@ public class MacOSKeychainTests { private const string TestNamespace = "git-test"; - [PlatformFact(Platforms.MacOS)] + [SkippablePlatformFact(Platforms.MacOS)] public void MacOSKeychain_ReadWriteDelete() { var keychain = new MacOSKeychain(TestNamespace); @@ -32,6 +34,19 @@ public void MacOSKeychain_ReadWriteDelete() Assert.Equal(account, outCredential.Account); Assert.Equal(password, outCredential.Password); } + // There is an unknown issue that the keychain can sometimes get itself in where all API calls + // result in an errSecAuthFailed error. The only solution seems to be a machine restart, which + // isn't really possible in CI! + // The problem has plagued others who are calling the same Keychain APIs from C# such as the + // MSAL.NET team - they don't know either. It might have something to do with the code signing + // signature of the binary (our collective best theory). + // It's probably only diagnosable at this point by Apple, but we don't have a reliable way to + // reproduce the problem. + // For now we will just mark the test as "skipped" when we hit this problem. + catch (InteropException iex) when (iex.ErrorCode == SecurityFramework.ErrorSecAuthFailed) + { + AssertEx.Skip("macOS Keychain is in an invalid state (errSecAuthFailed)"); + } finally { // Ensure we clean up after ourselves even in case of 'get' failures diff --git a/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs b/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs index 7ec5900f6..138bc665e 100644 --- a/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs +++ b/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs @@ -101,7 +101,7 @@ public CommandContext(string appPath) gitPath, FileSystem.GetCurrentDirectory() ); - Settings = new Settings(Environment, Git); + Settings = new WindowsSettings(Environment, Git, Trace); CredentialStore = new WindowsCredentialManager(Settings.CredentialNamespace); } else if (PlatformUtils.IsMacOS()) diff --git a/src/shared/Microsoft.Git.CredentialManager/Constants.cs b/src/shared/Microsoft.Git.CredentialManager/Constants.cs index 5751f5472..d8438fd29 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Constants.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Constants.cs @@ -107,6 +107,12 @@ public static class Remote } } + public static class WindowsRegistry + { + public const string HKAppBasePath = @"SOFTWARE\GitCredentialManager"; + public const string HKConfigurationPath = HKAppBasePath + @"\Configuration"; + } + public static class HelpUrls { public const string GcmProjectUrl = "https://aka.ms/gcmcore"; diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsSettings.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsSettings.cs new file mode 100644 index 000000000..663f09307 --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsSettings.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Git.CredentialManager.Interop.Windows +{ + /// + /// Reads settings from Git configuration, environment variables, and defaults from the Windows Registry. + /// + public class WindowsSettings : Settings + { + private readonly ITrace _trace; + + public WindowsSettings(IEnvironment environment, IGit git, ITrace trace) + : base(environment, git) + { + EnsureArgument.NotNull(trace, nameof(trace)); + _trace = trace; + + PlatformUtils.EnsureWindows(); + } + + protected override bool TryGetExternalDefault(string section, string property, out string value) + { + value = null; + +#if NETFRAMEWORK + // Check for machine (HKLM) registry keys that match the Git configuration name. + // These can be set by system administrators via Group Policy, so make useful defaults. + using (Win32.RegistryKey configKey = Win32.Registry.LocalMachine.OpenSubKey(Constants.WindowsRegistry.HKConfigurationPath)) + { + if (configKey is null) + { + // No configuration key exists + return false; + } + + string name = $"{section}.{property}"; + object registryValue = configKey.GetValue(name); + if (registryValue is null) + { + // No property exists + return false; + } + + value = registryValue.ToString(); + _trace.WriteLine($"Default setting found in registry: {name}={value}"); + + return true; + } +#else + return base.TryGetExternalDefault(section, property, out value); +#endif + } + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Settings.cs b/src/shared/Microsoft.Git.CredentialManager/Settings.cs index d3f7d5499..7d6ba6fb4 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Settings.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Settings.cs @@ -289,9 +289,29 @@ public IEnumerable GetSettingValues(string envarName, string section, st { yield return value; } + + // Check for an externally specified default value + if (TryGetExternalDefault(section, property, out string defaultValue)) + { + yield return defaultValue; + } } } + /// + /// Try to get the default value for a configuration setting. + /// This may come from external policies or the Operating System. + /// + /// Configuration section name. + /// Configuration property name. + /// Value of the configuration setting, or null. + /// True if a default setting has been set, false otherwise. + protected virtual bool TryGetExternalDefault(string section, string property, out string value) + { + value = null; + return false; + } + public Uri RemoteUri { get; set; } public bool IsDebuggingEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GcmDebug, false); diff --git a/src/shared/TestInfrastructure/AssertEx.cs b/src/shared/TestInfrastructure/AssertEx.cs new file mode 100644 index 000000000..563484148 --- /dev/null +++ b/src/shared/TestInfrastructure/AssertEx.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace Microsoft.Git.CredentialManager.Tests +{ + public static class AssertEx + { + /// + /// Requires the fact or theory be marked with the + /// or . + /// + /// Reason the test has been skipped. + public static void Skip(string reason) + { + Xunit.Skip.If(true, reason); + } + } +} diff --git a/src/shared/TestInfrastructure/PlatformAttributes.cs b/src/shared/TestInfrastructure/PlatformAttributes.cs index b95cd0f8e..99ec3b645 100644 --- a/src/shared/TestInfrastructure/PlatformAttributes.cs +++ b/src/shared/TestInfrastructure/PlatformAttributes.cs @@ -28,6 +28,28 @@ public PlatformTheoryAttribute(Platforms platforms) } } + public class SkippablePlatformFactAttribute : SkippableFactAttribute + { + public SkippablePlatformFactAttribute(Platforms platforms) + { + Xunit.Skip.IfNot( + XunitHelpers.IsSupportedPlatform(platforms), + "Test not supported on this platform." + ); + } + } + + public class SkippablePlatformTheoryAttribute : SkippableTheoryAttribute + { + public SkippablePlatformTheoryAttribute(Platforms platforms) + { + Xunit.Skip.IfNot( + XunitHelpers.IsSupportedPlatform(platforms), + "Test not supported on this platform." + ); + } + } + internal static class XunitHelpers { public static bool IsSupportedPlatform(Platforms platforms) diff --git a/src/shared/TestInfrastructure/TestInfrastructure.csproj b/src/shared/TestInfrastructure/TestInfrastructure.csproj index d4376b8e8..fcf2af69e 100644 --- a/src/shared/TestInfrastructure/TestInfrastructure.csproj +++ b/src/shared/TestInfrastructure/TestInfrastructure.csproj @@ -11,6 +11,7 @@ +