diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index 97544a33f..bfdc7e302 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -55,38 +55,7 @@ those of the [Git configuration][config] settings. The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` (integer). -## macOS - -Default settings values come from macOS's preferences system. Configuration -profiles can be deployed to devices using a compatible Mobile Device Management -(MDM) solution. - -Configuration for Git Credential Manager must take the form of a dictionary, set -for the domain `git-credential-manager` under the key `configuration`. For -example: - -```shell -defaults write git-credential-manager configuration -dict-add -``` - -..where `` is the name of the settings from the [Git configuration][config] -reference, and `` is the desired value. - -All values in the `configuration` dictionary must be strings. For boolean values -use `true` or `false`, and for integer values use the number in string form. - -To read the current configuration: - -```console -$ defaults read git-credential-manager configuration -{ - = ; - ... - = ; -} -``` - -## Linux +## macOS/Linux Default configuration setting stores has not been implemented. diff --git a/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs b/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs deleted file mode 100644 index 0efb14471..000000000 --- a/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Xunit; -using GitCredentialManager.Interop.MacOS; -using static GitCredentialManager.Tests.TestUtils; - -namespace GitCredentialManager.Tests.Interop.MacOS; - -public class MacOSPreferencesTests -{ - private const string TestAppId = "com.example.gcm-test"; - private const string DefaultsPath = "/usr/bin/defaults"; - - [MacOSFact] - public async Task MacOSPreferences_ReadPreferences() - { - try - { - await SetupTestPreferencesAsync(); - - var pref = new MacOSPreferences(TestAppId); - - // Exists - string stringValue = pref.GetString("myString"); - int? intValue = pref.GetInteger("myInt"); - IDictionary dictValue = pref.GetDictionary("myDict"); - - Assert.NotNull(stringValue); - Assert.Equal("this is a string", stringValue); - Assert.NotNull(intValue); - Assert.Equal(42, intValue); - Assert.NotNull(dictValue); - Assert.Equal(2, dictValue.Count); - Assert.Equal("value1", dictValue["dict-k1"]); - Assert.Equal("value2", dictValue["dict-k2"]); - - // Does not exist - string missingString = pref.GetString("missingString"); - int? missingInt = pref.GetInteger("missingInt"); - IDictionary missingDict = pref.GetDictionary("missingDict"); - - Assert.Null(missingString); - Assert.Null(missingInt); - Assert.Null(missingDict); - } - finally - { - await CleanupTestPreferencesAsync(); - } - } - - private static async Task SetupTestPreferencesAsync() - { - // Using the defaults command set up preferences for the test app - await RunCommandAsync(DefaultsPath, $"write {TestAppId} myString \"this is a string\""); - await RunCommandAsync(DefaultsPath, $"write {TestAppId} myInt -int 42"); - await RunCommandAsync(DefaultsPath, $"write {TestAppId} myDict -dict dict-k1 value1 dict-k2 value2"); - } - - private static async Task CleanupTestPreferencesAsync() - { - // Delete the test app preferences - // defaults delete com.example.gcm-test - await RunCommandAsync(DefaultsPath, $"delete {TestAppId}"); - } -} diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index d3ef1dbf6..712db32e1 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -131,7 +131,7 @@ public CommandContext() gitPath, FileSystem.GetCurrentDirectory() ); - Settings = new MacOSSettings(Environment, Git, Trace); + Settings = new Settings(Environment, Git); } else if (PlatformUtils.IsLinux()) { diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 4777b0cf8..191fcc83d 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -16,7 +16,6 @@ public static class Constants public const string GcmDataDirectoryName = ".gcm"; - public const string MacOSBundleId = "git-credential-manager"; public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf"); /// diff --git a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs index 9335e136d..b024be129 100644 --- a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs +++ b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs @@ -302,18 +302,35 @@ private static string GetStringAttribute(IntPtr dict, IntPtr key) return null; } - if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero) + IntPtr buffer = IntPtr.Zero; + try { - if (CFGetTypeID(value) == CFStringGetTypeID()) + if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero) { - return CFStringToString(value); + if (CFGetTypeID(value) == CFStringGetTypeID()) + { + int stringLength = (int)CFStringGetLength(value); + int bufferSize = stringLength + 1; + buffer = Marshal.AllocHGlobal(bufferSize); + if (CFStringGetCString(value, buffer, bufferSize, CFStringEncoding.kCFStringEncodingUTF8)) + { + return Marshal.PtrToStringAuto(buffer, stringLength); + } + } + + if (CFGetTypeID(value) == CFDataGetTypeID()) + { + int length = CFDataGetLength(value); + IntPtr ptr = CFDataGetBytePtr(value); + return Marshal.PtrToStringAuto(ptr, length); + } } - - if (CFGetTypeID(value) == CFDataGetTypeID()) + } + finally + { + if (buffer != IntPtr.Zero) { - int length = CFDataGetLength(value); - IntPtr ptr = CFDataGetBytePtr(value); - return Marshal.PtrToStringAuto(ptr, length); + Marshal.FreeHGlobal(buffer); } } diff --git a/src/shared/Core/Interop/MacOS/MacOSPreferences.cs b/src/shared/Core/Interop/MacOS/MacOSPreferences.cs deleted file mode 100644 index f866b30a8..000000000 --- a/src/shared/Core/Interop/MacOS/MacOSPreferences.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using GitCredentialManager.Interop.MacOS.Native; -using static GitCredentialManager.Interop.MacOS.Native.CoreFoundation; - -namespace GitCredentialManager.Interop.MacOS; - -public class MacOSPreferences -{ - private readonly string _appId; - - public MacOSPreferences(string appId) - { - EnsureArgument.NotNull(appId, nameof(appId)); - - _appId = appId; - } - - /// - /// Return a typed value from the app preferences. - /// - /// Preference name. - /// Thrown if the preference is not a string. - /// - /// or null if the preference with the given key does not exist. - /// - public string GetString(string key) - { - return TryGet(key, CFStringToString, out string value) - ? value - : null; - } - - /// - /// Return a typed value from the app preferences. - /// - /// Preference name. - /// Thrown if the preference is not an integer. - /// - /// or null if the preference with the given key does not exist. - /// - public int? GetInteger(string key) - { - return TryGet(key, CFNumberToInt32, out int value) - ? value - : null; - } - - /// - /// Return a typed value from the app preferences. - /// - /// Preference name. - /// Thrown if the preference is not a dictionary. - /// - /// or null if the preference with the given key does not exist. - /// - public IDictionary GetDictionary(string key) - { - return TryGet(key, CFDictionaryToDictionary, out IDictionary value) - ? value - : null; - } - - private bool TryGet(string key, Func converter, out T value) - { - IntPtr cfValue = IntPtr.Zero; - IntPtr keyPtr = IntPtr.Zero; - IntPtr appIdPtr = CreateAppIdPtr(); - - try - { - keyPtr = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.kCFStringEncodingUTF8); - cfValue = CFPreferencesCopyAppValue(keyPtr, appIdPtr); - - if (cfValue == IntPtr.Zero) - { - value = default; - return false; - } - - value = converter(cfValue); - return true; - } - finally - { - if (cfValue != IntPtr.Zero) CFRelease(cfValue); - if (keyPtr != IntPtr.Zero) CFRelease(keyPtr); - if (appIdPtr != IntPtr.Zero) CFRelease(appIdPtr); - } - } - - private IntPtr CreateAppIdPtr() - { - return CFStringCreateWithCString(IntPtr.Zero, _appId, CFStringEncoding.kCFStringEncodingUTF8); - } -} diff --git a/src/shared/Core/Interop/MacOS/MacOSSettings.cs b/src/shared/Core/Interop/MacOS/MacOSSettings.cs deleted file mode 100644 index 3ef2c8247..000000000 --- a/src/shared/Core/Interop/MacOS/MacOSSettings.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace GitCredentialManager.Interop.MacOS -{ - /// - /// Reads settings from Git configuration, environment variables, and defaults from the system. - /// - public class MacOSSettings : Settings - { - private readonly ITrace _trace; - - public MacOSSettings(IEnvironment environment, IGit git, ITrace trace) - : base(environment, git) - { - EnsureArgument.NotNull(trace, nameof(trace)); - _trace = trace; - - PlatformUtils.EnsureMacOS(); - } - - protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) - { - value = null; - - try - { - // Check for app default preferences for our bundle ID. - // Defaults can be deployed system administrators via device management profiles. - var prefs = new MacOSPreferences(Constants.MacOSBundleId); - IDictionary dict = prefs.GetDictionary("configuration"); - - if (dict is null) - { - // No configuration key exists - return false; - } - - // Wrap the raw dictionary in one configured with the Git configuration key comparer. - // This means we can use the same key comparison rules as Git in our configuration plist dict, - // That is, sections and names are insensitive to case, but the scope is case-sensitive. - var config = new Dictionary(dict, GitConfigurationKeyComparer.Instance); - - string name = string.IsNullOrWhiteSpace(scope) - ? $"{section}.{property}" - : $"{section}.{scope}.{property}"; - - if (!config.TryGetValue(name, out value)) - { - // No property exists - return false; - } - - _trace.WriteLine($"Default setting found in app preferences: {name}={value}"); - return true; - } - catch (Exception ex) - { - // Reading defaults is not critical to the operation of the application - // so we can ignore any errors and just log the failure. - _trace.WriteLine("Failed to read default setting from app preferences."); - _trace.WriteException(ex); - return false; - } - } - } -} diff --git a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs index 9cab2ca8f..0f32a383b 100644 --- a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs +++ b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; using static GitCredentialManager.Interop.MacOS.Native.LibSystem; @@ -56,9 +55,6 @@ public static extern void CFDictionaryAddValue( public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, long numBytes, CFStringEncoding encoding, bool isExternalRepresentation); - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string cStr, CFStringEncoding encoding); - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern long CFStringGetLength(IntPtr theString); @@ -86,130 +82,15 @@ public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int CFArrayGetTypeID(); - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int CFNumberGetTypeID(); - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr CFDataGetBytePtr(IntPtr theData); [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int CFDataGetLength(IntPtr theData); - - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CFPreferencesCopyAppValue(IntPtr key, IntPtr appID); - - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern bool CFNumberGetValue(IntPtr number, CFNumberType theType, out IntPtr valuePtr); - - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CFDictionaryGetKeysAndValues(IntPtr theDict, IntPtr[] keys, IntPtr[] values); - - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern long CFDictionaryGetCount(IntPtr theDict); - - public static string CFStringToString(IntPtr cfString) - { - if (cfString == IntPtr.Zero) - { - throw new ArgumentNullException(nameof(cfString)); - } - - if (CFGetTypeID(cfString) != CFStringGetTypeID()) - { - throw new InvalidOperationException("Object is not a CFString."); - } - - long length = CFStringGetLength(cfString); - IntPtr buffer = Marshal.AllocHGlobal((int)length + 1); - - try - { - if (!CFStringGetCString(cfString, buffer, length + 1, CFStringEncoding.kCFStringEncodingUTF8)) - { - throw new InvalidOperationException("Failed to convert CFString to C string."); - } - - return Marshal.PtrToStringAnsi(buffer); - } - finally - { - Marshal.FreeHGlobal(buffer); - } - } - - public static int CFNumberToInt32(IntPtr cfNumber) - { - if (cfNumber == IntPtr.Zero) - { - throw new ArgumentNullException(nameof(cfNumber)); - } - - if (CFGetTypeID(cfNumber) != CFNumberGetTypeID()) - { - throw new InvalidOperationException("Object is not a CFNumber."); - } - - if (!CFNumberGetValue(cfNumber, CFNumberType.kCFNumberIntType, out IntPtr valuePtr)) - { - throw new InvalidOperationException("Failed to convert CFNumber to Int32."); - } - - return valuePtr.ToInt32(); - } - - public static IDictionary CFDictionaryToDictionary(IntPtr cfDict) - { - if (cfDict == IntPtr.Zero) - { - throw new ArgumentNullException(nameof(cfDict)); - } - - if (CFGetTypeID(cfDict) != CFDictionaryGetTypeID()) - { - throw new InvalidOperationException("Object is not a CFDictionary."); - } - - int count = (int)CFDictionaryGetCount(cfDict); - var keys = new IntPtr[count]; - var values = new IntPtr[count]; - - CFDictionaryGetKeysAndValues(cfDict, keys, values); - - var dict = new Dictionary(capacity: count); - for (int i = 0; i < count; i++) - { - string keyStr = CFStringToString(keys[i])!; - string valueStr = CFStringToString(values[i]); - - dict[keyStr] = valueStr; - } - - return dict; - } } public enum CFStringEncoding { kCFStringEncodingUTF8 = 0x08000100, } - - public enum CFNumberType - { - kCFNumberSInt8Type = 1, - kCFNumberSInt16Type = 2, - kCFNumberSInt32Type = 3, - kCFNumberSInt64Type = 4, - kCFNumberFloat32Type = 5, - kCFNumberFloat64Type = 6, - kCFNumberCharType = 7, - kCFNumberShortType = 8, - kCFNumberIntType = 9, - kCFNumberLongType = 10, - kCFNumberLongLongType = 11, - kCFNumberFloatType = 12, - kCFNumberDoubleType = 13, - kCFNumberCFIndexType = 14, - kCFNumberNSIntegerType = 15, - kCFNumberCGFloatType = 16 - } } diff --git a/src/shared/TestInfrastructure/TestUtils.cs b/src/shared/TestInfrastructure/TestUtils.cs index 000b8e75e..c547856d7 100644 --- a/src/shared/TestInfrastructure/TestUtils.cs +++ b/src/shared/TestInfrastructure/TestUtils.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.IO; -using System.Threading.Tasks; namespace GitCredentialManager.Tests { @@ -89,41 +87,5 @@ public static string GetUuid(int length = -1) return uuid.Substring(0, length); } - - public static async Task RunCommandAsync(string filePath, string arguments, string workingDirectory = null) - { - using var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = filePath, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory - } - }; - - process.Start(); - - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - { - throw new InvalidOperationException( - $"Command `{filePath} {arguments}` failed with exit code {process.ExitCode}." + - Environment.NewLine + - $"Output: {output}" + - Environment.NewLine + - $"Error: {error}"); - } - - return output; - } } }