-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add basic framework for Git Credential Manager #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
549374f
3923d4e
876d131
0f89110
a6a1752
2cb66a1
d0f6beb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -328,3 +328,6 @@ ASALocalRun/ | |
|
|
||
| # MFractors (Xamarin productivity tool) working folder | ||
| .mfractor/ | ||
|
|
||
| # macOS Finder files | ||
| .DS_Store | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A7FC1234-95E3-4496-B5F7-4306F41E6A0E}" | ||
| EndProject | ||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4B305AC9-153F-4EA3-822F-3E5023BABAF1}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "git-credential-manager", "src\git-credential-manager\git-credential-manager.csproj", "{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Git.CredentialManager", "src\Microsoft.Git.CredentialManager\Microsoft.Git.CredentialManager.csproj", "{31BCFC70-B767-4274-873F-1A076D422FC3}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Git.CredentialManager.Tests", "tests\Microsoft.Git.CredentialManager.Tests\Microsoft.Git.CredentialManager.Tests.csproj", "{AD41FA1E-51F5-4E4F-B7DA-32F921491313}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(NestedProjects) = preSolution | ||
| {28F06D44-AB25-4CF5-93F9-978C23FAA9D6} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} | ||
| {31BCFC70-B767-4274-873F-1A076D422FC3} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} | ||
| {AD41FA1E-51F5-4E4F-B7DA-32F921491313} = {4B305AC9-153F-4EA3-822F-3E5023BABAF1} | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| EndGlobal |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| using System; | ||
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Text; | ||
|
|
||
| namespace Microsoft.Git.CredentialManager | ||
| { | ||
| /// <summary> | ||
| /// Represents the execution environment for a Git credential helper command. | ||
| /// </summary> | ||
| public interface ICommandContext | ||
| { | ||
| /// <summary> | ||
| /// The standard input text stream from the calling process, typically Git. | ||
| /// </summary> | ||
| TextReader StdIn { get; } | ||
|
|
||
| /// <summary> | ||
| /// The standard output text stream connected back to the calling process, typically Git. | ||
| /// </summary> | ||
| TextWriter StdOut { get; } | ||
|
|
||
| /// <summary> | ||
| /// The standard error text stream connected back to the calling process, typically Git. | ||
| /// </summary> | ||
| TextWriter StdError { get; } | ||
|
|
||
| /// <summary> | ||
| /// Application tracing system. | ||
| /// </summary> | ||
| ITrace Trace { get; } | ||
|
|
||
| /// <summary> | ||
| /// File system abstraction (exists mainly for testing). | ||
| /// </summary> | ||
| IFileSystem FileSystem { get; } | ||
|
|
||
| /// <summary> | ||
| /// Access the environment variables for the current GCM process. | ||
| /// </summary> | ||
| /// <returns>Set of all current environment variables.</returns> | ||
| IReadOnlyDictionary<string, string> GetEnvironmentVariables(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Real command execution environment using the actual <see cref="Console"/>, file system calls and environment. | ||
| /// </summary> | ||
| public class CommandContext : ICommandContext | ||
| { | ||
| private const string LineFeed = "\n"; | ||
|
|
||
| private static readonly Encoding Utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); | ||
|
|
||
| private TextReader _stdIn; | ||
| private TextWriter _stdOut; | ||
| private TextWriter _stdErr; | ||
|
|
||
| #region ICommandContext | ||
|
|
||
| public TextReader StdIn | ||
| { | ||
| get | ||
| { | ||
| if (_stdIn == null) | ||
| { | ||
| _stdIn = new StreamReader(Console.OpenStandardInput(), Utf8NoBomEncoding); | ||
| } | ||
|
|
||
| return _stdIn; | ||
| } | ||
| } | ||
|
|
||
| public TextWriter StdOut | ||
| { | ||
| get | ||
| { | ||
| if (_stdOut == null) | ||
| { | ||
| _stdOut = new StreamWriter(Console.OpenStandardOutput(), Utf8NoBomEncoding) | ||
| { | ||
| AutoFlush = true, | ||
| NewLine = LineFeed, | ||
| }; | ||
| } | ||
|
|
||
| return _stdOut; | ||
| } | ||
| } | ||
|
|
||
| public TextWriter StdError | ||
| { | ||
| get | ||
| { | ||
| if (_stdErr == null) | ||
| { | ||
| _stdErr = new StreamWriter(Console.OpenStandardError(), Utf8NoBomEncoding) | ||
| { | ||
| AutoFlush = true, | ||
| NewLine = LineFeed, | ||
| }; | ||
| } | ||
|
|
||
| return _stdErr; | ||
| } | ||
| } | ||
|
|
||
| public ITrace Trace { get; } = new Trace(); | ||
|
|
||
| public IFileSystem FileSystem { get; } = new FileSystem(); | ||
|
|
||
| public IReadOnlyDictionary<string, string> GetEnvironmentVariables() | ||
| { | ||
| IDictionary variables = Environment.GetEnvironmentVariables(); | ||
|
|
||
| // On Windows it is technically possible to get env vars which differ only by case | ||
| // even though the general assumption is that they are case insensitive on Windows. | ||
| // For example, some of the standard .NET types like System.Diagnostics.Process | ||
| // will fail to start a process on Windows if given duplicate environment variables. | ||
| // See this issue for more information: https://github.com/dotnet/corefx/issues/13146 | ||
|
|
||
| // If we're on the Windows platform we should de-duplicate by setting the string | ||
| // comparer to OrdinalIgnoreCase. | ||
| var comparer = PlatformUtils.IsWindows() | ||
| ? StringComparer.OrdinalIgnoreCase | ||
| : StringComparer.Ordinal; | ||
|
|
||
| var result = new Dictionary<string, string>(comparer); | ||
|
|
||
| foreach (var key in variables.Keys) | ||
| { | ||
| if (key is string name && variables[key] is string value) | ||
| { | ||
| result[name] = value; | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| #endregion | ||
| } | ||
|
|
||
| public static class CommandContextExtensions | ||
| { | ||
| /// <summary> | ||
| /// Try to get the current value of the specified environment variable. | ||
| /// </summary> | ||
| /// <param name="context"><see cref="ICommandContext"/></param> | ||
| /// <param name="key">The name of environment variable.</param> | ||
| /// <param name="value">The current value of the environment variable.</param> | ||
| /// <returns>True if the environment variable was set and has a value, false otherwise.</returns> | ||
| public static bool TryGetEnvironmentVariable(this ICommandContext context, string key, out string value) | ||
| { | ||
| return context.GetEnvironmentVariables().TryGetValue(key, out value); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Test if the specified environment variable is 'truthy' (considered to be equivalent to a 'true' value | ||
| /// by <see cref="StringExtensions.IsTruthy"/>). | ||
| /// </summary> | ||
| /// <param name="context"><see cref="ICommandContext"/></param> | ||
| /// <param name="key">The name of environment variable.</param> | ||
| /// <param name="defaultValue"> | ||
| /// The assumed default value of the environment variable if it does not | ||
| /// exist/has not been set. | ||
| /// </param> | ||
| /// <returns>True if the environment variable was set and has a 'truthy' value, <see cref="defaultValue"/> otherwise.</returns> | ||
| public static bool IsEnvironmentVariableTruthy(this ICommandContext context, string key, bool defaultValue) | ||
| { | ||
| if (context.TryGetEnvironmentVariable(key, out string valueStr) && valueStr.IsTruthy()) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return defaultValue; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Microsoft.Git.CredentialManager.Commands | ||
| { | ||
| /// <summary> | ||
| /// Represents a Git Credential Manager command. | ||
| /// </summary> | ||
| public abstract class CommandBase | ||
| { | ||
| /// <summary> | ||
| /// Check if this command should be executed for the given arguments. | ||
| /// </summary> | ||
| /// <param name="args">Application command-line arguments.</param> | ||
| /// <returns>True if the command should be executed, false otherwise.</returns> | ||
| public abstract bool CanExecute(string[] args); | ||
|
|
||
| /// <summary> | ||
| /// Execute the command. | ||
| /// </summary> | ||
| /// <param name="context">The current command execution context.</param> | ||
| /// <param name="args">Application command-line arguments.</param> | ||
| /// <returns>Awaitable task for the command execution.</returns> | ||
| public abstract Task ExecuteAsync(ICommandContext context, string[] args); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Represents a simple Git Credential Manager command that takes a single named verb. | ||
| /// </summary> | ||
| public abstract class VerbCommandBase : CommandBase | ||
| { | ||
| /// <summary> | ||
| /// Name of the command verb. | ||
| /// </summary> | ||
| protected abstract string Name { get; } | ||
|
|
||
| public override bool CanExecute(string[] args) | ||
| { | ||
| return args.Length != 0 && StringComparer.OrdinalIgnoreCase.Equals(args[0], Name); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would you expect to have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we're invoked with any command which doesn't match The The |
||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Represents a command which selects a <see cref="IHostProvider"/> from a <see cref="IHostProviderRegistry"/> | ||
| /// based on the <see cref="InputArguments"/> from standard input, and interacts with a <see cref="GitCredential"/>. | ||
| /// </summary> | ||
| public abstract class HostProviderCommandBase : VerbCommandBase | ||
| { | ||
| private readonly IHostProviderRegistry _hostProviderRegistry; | ||
|
|
||
| protected HostProviderCommandBase(IHostProviderRegistry hostProviderRegistry) | ||
| { | ||
| _hostProviderRegistry = hostProviderRegistry; | ||
| } | ||
|
|
||
| public override async Task ExecuteAsync(ICommandContext context, string[] args) | ||
| { | ||
| // Parse standard input arguments | ||
| // git-credential treats the keys as case-sensitive; so should we. | ||
| IDictionary<string, string> inputDict = await context.StdIn.ReadDictionaryAsync(StringComparer.Ordinal); | ||
| var input = new InputArguments(inputDict); | ||
|
|
||
| // Determine the host provider | ||
| context.Trace.WriteLine("Detecting host provider for input:"); | ||
| context.Trace.WriteDictionary(inputDict); | ||
| IHostProvider provider = _hostProviderRegistry.GetProvider(input); | ||
| context.Trace.WriteLine($"Host provider '{provider.Name}' was selected."); | ||
|
|
||
| // Build the credential identifier | ||
| string hostProviderKey = provider.GetCredentialKey(input); | ||
| string credentialKey = $"git:{hostProviderKey}"; | ||
| context.Trace.WriteLine($"Credential key is '{credentialKey}'."); | ||
|
|
||
| await ExecuteInternalAsync(context, input, provider, credentialKey); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Execute the command using the given <see cref="InputArguments"/>, <see cref="IHostProvider"/>, and <see cref="GitCredential"/> key. | ||
| /// </summary> | ||
| /// <param name="context">The current command execution context.</param> | ||
| /// <param name="input">Input arguments of the current Git credential query.</param> | ||
| /// <param name="provider">Host provider for the current <see cref="InputArguments"/>.</param> | ||
| /// <param name="credentialKey">Unique identifier in the OS secure storage system for a <see cref="GitCredential"/>.</param> | ||
| /// <returns>Awaitable task for the command execution.</returns> | ||
| protected abstract Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider, string credentialKey); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Microsoft.Git.CredentialManager.Commands | ||
| { | ||
| /// <summary> | ||
| /// Erase a previously stored <see cref="GitCredential"/> from the OS secure credential store. | ||
| /// </summary> | ||
| public class EraseCommand : HostProviderCommandBase | ||
| { | ||
| public EraseCommand(IHostProviderRegistry hostProviderRegistry) | ||
| : base(hostProviderRegistry) { } | ||
|
|
||
| protected override string Name => "erase"; | ||
|
|
||
| protected override Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider, string credentialKey) | ||
| { | ||
| throw new NotImplementedException(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not implemented because this is a base class? or because it hasn't been implemented yet?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not implemented because it's not implemented (in this PR) yet. I'm intentionally holding back some of the code of the core credential helper commands and host providers to keep this PR more manageable; they're coming in subsequent PRs. I expect this initial PR to be the largest. |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Microsoft.Git.CredentialManager.Commands | ||
| { | ||
| /// <summary> | ||
| /// Acquire a new <see cref="GitCredential"/> from a <see cref="IHostProvider"/>. | ||
| /// </summary> | ||
| public class GetCommand : HostProviderCommandBase | ||
| { | ||
| public GetCommand(IHostProviderRegistry hostProviderRegistry) | ||
| : base(hostProviderRegistry) { } | ||
|
|
||
| protected override string Name => "get"; | ||
|
|
||
| protected override Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider, string credentialKey) | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you actually expect to get key / values that are not strings? or are you being defensive here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I do not, and yes this is partly for defensive purposes 😀
Also since the type you get back from
Environment.GetEnvironmentVariables()is the non-genericIDictionaryand I want to offer the friendlier and more useful (IMO) genericIDictionary<string, string>, this syntax offers a nice way to both safely cast theobject/objectkey-value pair tostringsthat are also non-null(anullvalue should be 'not set'/'not present').