Skip to content

Commit 6723971

Browse files
authored
Merge branch 'master' into net5.0
2 parents 6ed017e + 96944cb commit 6723971

File tree

9 files changed

+260
-6
lines changed

9 files changed

+260
-6
lines changed

docs/configuration.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ Value|Credential Store
201201
_(unset)_|(error)
202202
`secretservice`|[freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) via [libsecret](https://wiki.gnome.org/Projects/Libsecret) (requires a graphical interface to unlock secret collections).
203203
`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility](https://www.passwordstore.org/) (requires GPG and `pass` to initialize the store).
204+
`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).
204205
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`](#credentialplaintextstorepath).
205206

206207
##### Example
@@ -213,6 +214,28 @@ git config --global credential.credentialStore gpg
213214

214215
---
215216

217+
### credential.cacheOptions
218+
219+
Pass [options](https://git-scm.com/docs/git-credential-cache#_options)
220+
to the Git credential cache when
221+
[`credential.credentialStore`](#credentialcredentialstore)
222+
is set to `cache`. This allows you to select a different amount
223+
of time to cache credentials (the default is 900 seconds) by passing
224+
`"--timeout <seconds>"`. Use of other options like `--socket` is untested
225+
and unsupported, but there's no reason it shouldn't work.
226+
227+
Defaults to empty.
228+
229+
#### Example
230+
231+
```shell
232+
git config --global credential.cacheOptions "--timeout 300"
233+
```
234+
235+
**Also see: [GCM_CREDENTIAL_CACHE_OPTIONS](environment.md#GCM_CREDENTIAL_CACHE_OPTIONS)**
236+
237+
---
238+
216239
### credential.plaintextStorePath
217240

218241
Specify a custom directory to store plaintext credential files in when [`credential.credentialStore`](#credentialcredentialstore) is set to `plaintext`.

docs/environment.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ Value|Credential Store
348348
_(unset)_|(error)
349349
`secretservice`|[freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/) via [libsecret](https://wiki.gnome.org/Projects/Libsecret) (requires a graphical interface to unlock secret collections).
350350
`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility](https://www.passwordstore.org/) (requires GPG and `pass` to initialize the store).
351+
`cache`|Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache).
351352
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`](#GCM_PLAINTEXT_STORE_PATH).
352353

353354
##### Linux
@@ -360,6 +361,27 @@ export GCM_CREDENTIAL_STORE="gpg"
360361

361362
---
362363

364+
### GCM_CREDENTIAL_CACHE_OPTIONS
365+
366+
Pass [options](https://git-scm.com/docs/git-credential-cache#_options)
367+
to the Git credential cache when [`GCM_CREDENTIAL_STORE`](#GCM_CREDENTIAL_STORE)
368+
is set to `cache`. This allows you to select a different amount
369+
of time to cache credentials (the default is 900 seconds) by passing
370+
`"--timeout <seconds>"`. Use of other options like `--socket` is untested
371+
and unsupported, but there's no reason it shouldn't work.
372+
373+
Defaults to empty.
374+
375+
#### Linux
376+
377+
```shell
378+
export GCM_CREDENTIAL_CACHE_OPTIONS="--timeout 300"
379+
```
380+
381+
**Also see: [credential.cacheOptions](configuration.md#credentialcacheoptions)**
382+
383+
---
384+
363385
### GCM_PLAINTEXT_STORE_PATH
364386

365387
Specify a custom directory to store plaintext credential files in when [`GCM_CREDENTIAL_STORE`](#GCM_CREDENTIAL_STORE) is set to `plaintext`.

docs/linuxcredstores.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Credential stores on Linux
22

3-
There are currently three options for storing credentials that Git Credential
3+
There are four options for storing credentials that Git Credential
44
Manager Core (GCM Core) manages on Linux platforms:
55

66
1. [freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/)
77
2. GPG/[`pass`](https://www.passwordstore.org/) compatible files
8-
3. Plaintext files
8+
3. Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache)
9+
4. Plaintext files
910

1011
By default, GCM Core comes unconfigured. You can select which credential store
1112
to use by setting the [`GCM_CREDENTIAL_STORE`](environment.md#GCM_CREDENTIAL_STORE)
@@ -87,7 +88,37 @@ export GPG_TTY=$(tty)
8788
**Note:** Using `/dev/tty` does not appear to work here - you must use the real
8889
TTY device path, as returned by the `tty` utility.
8990

90-
## 3. Plaintext files
91+
## 3. Git's built-in [credential cache](https://git-scm.com/docs/git-credential-cache)
92+
93+
```shell
94+
export GCM_CREDENTIAL_STORE=cache
95+
# or
96+
git config --global credential.credentialStore cache
97+
```
98+
99+
This credential store uses Git's built-in ephemeral
100+
[in-memory credential cache](https://git-scm.com/docs/git-credential-cache).
101+
This helps you reduce the number of times you have to authenticate but
102+
doesn't require storing credentials on persistent storage. It's good for
103+
scenarios like [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/overview) or [AWS CloudShell](https://aws.amazon.com/cloudshell/), where you
104+
don't want to leave credentials on disk but also don't want to re-authenticate
105+
on every Git operation.
106+
107+
By default, `git credential-cache` stores your credentials for 900 seconds.
108+
That, and any other
109+
[options it accepts](https://git-scm.com/docs/git-credential-cache#_options),
110+
may be altered by setting them in the environment variable
111+
`GCM_CREDENTIAL_CACHE_OPTIONS` or the Git config value
112+
`credential.cacheOptions`. (Using the `--socket` option is untested
113+
and unsupported, but there's no reason it shouldn't work.)
114+
115+
```shell
116+
export GCM_CREDENTIAL_CACHE_OPTIONS="--timeout 300"
117+
# or
118+
git config --global credential.cacheOptions "--timeout 300"
119+
```
120+
121+
## 4. Plaintext files
91122

92123
```shell
93124
export GCM_CREDENTIAL_STORE=plaintext

src/shared/Microsoft.Git.CredentialManager/CommandContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public CommandContext()
131131
Environment.LocateExecutable("gpg"),
132132
SessionManager
133133
);
134-
CredentialStore = new LinuxCredentialStore(FileSystem, Settings, SessionManager, gpg, Environment);
134+
CredentialStore = new LinuxCredentialStore(FileSystem, Settings, SessionManager, gpg, Environment, Git);
135135
}
136136
else
137137
{

src/shared/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static class EnvironmentVariables
5353
public const string MsAuthFlow = "GCM_MSAUTH_FLOW";
5454
public const string GcmCredNamespace = "GCM_NAMESPACE";
5555
public const string GcmCredentialStore = "GCM_CREDENTIAL_STORE";
56+
public const string GcmCredCacheOptions = "GCM_CREDENTIAL_CACHE_OPTIONS";
5657
public const string GcmPlaintextStorePath = "GCM_PLAINTEXT_STORE_PATH";
5758
public const string GitExecutablePath = "GIT_EXEC_PATH";
5859
}
@@ -83,6 +84,7 @@ public static class Credential
8384
public const string MsAuthFlow = "msauthFlow";
8485
public const string CredNamespace = "namespace";
8586
public const string CredentialStore = "credentialStore";
87+
public const string CredCacheOptions = "cacheOptions";
8688
public const string PlaintextStorePath = "plaintextStorePath";
8789
}
8890

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System.Collections.Generic;
4+
5+
namespace Microsoft.Git.CredentialManager
6+
{
7+
public class CredentialCacheStore : ICredentialStore
8+
{
9+
readonly IGit _git;
10+
readonly string _options;
11+
12+
public CredentialCacheStore(IGit git, string options)
13+
{
14+
_git = git;
15+
if (string.IsNullOrEmpty(options))
16+
{
17+
_options = string.Empty;
18+
}
19+
else
20+
{
21+
_options = options;
22+
}
23+
}
24+
25+
#region ICredentialStore
26+
27+
public ICredential Get(string service, string account)
28+
{
29+
var input = MakeGitCredentialsEntry(service, account);
30+
31+
var result = _git.InvokeHelperAsync(
32+
$"credential-cache get {_options}",
33+
input
34+
).GetAwaiter().GetResult();
35+
36+
if (result.ContainsKey("username") && result.ContainsKey("password"))
37+
{
38+
return new GitCredential(result["username"], result["password"]);
39+
}
40+
41+
return null;
42+
}
43+
44+
public void AddOrUpdate(string service, string account, string secret)
45+
{
46+
var input = MakeGitCredentialsEntry(service, account);
47+
input["password"] = secret;
48+
49+
// per https://git-scm.com/docs/gitcredentials :
50+
// For a store or erase operation, the helper’s output is ignored.
51+
_git.InvokeHelperAsync(
52+
$"credential-cache store {_options}",
53+
input
54+
).GetAwaiter().GetResult();
55+
}
56+
57+
public bool Remove(string service, string account)
58+
{
59+
var input = MakeGitCredentialsEntry(service, account);
60+
61+
// per https://git-scm.com/docs/gitcredentials :
62+
// For a store or erase operation, the helper’s output is ignored.
63+
_git.InvokeHelperAsync(
64+
$"credential-cache erase {_options}",
65+
input
66+
).GetAwaiter().GetResult();
67+
68+
// the credential cache doesn't tell us whether anything was erased
69+
// but we're optimistic sorts
70+
return true;
71+
}
72+
73+
#endregion
74+
75+
private Dictionary<string, string> MakeGitCredentialsEntry(string service, string account)
76+
{
77+
var result = new Dictionary<string, string>();
78+
79+
result["url"] = service;
80+
if (!string.IsNullOrEmpty(account))
81+
{
82+
result["username"] = account;
83+
}
84+
85+
return result;
86+
}
87+
}
88+
}

src/shared/Microsoft.Git.CredentialManager/Git.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System;
4+
using System.Collections.Generic;
35
using System.Diagnostics;
6+
using System.Threading.Tasks;
47

58
namespace Microsoft.Git.CredentialManager
69
{
@@ -12,6 +15,14 @@ public interface IGit
1215
/// <param name="level">Configuration level filter.</param>
1316
/// <returns>Git configuration.</returns>
1417
IGitConfiguration GetConfiguration(GitConfigurationLevel level);
18+
19+
/// <summary>
20+
/// Run a Git helper process which expects and returns key-value maps
21+
/// </summary>
22+
/// <param name="args">Arguments to the executable</param>
23+
/// <param name="standardInput">key-value map to pipe into stdin</param>
24+
/// <returns>stdout from helper executable as key-value map</returns>
25+
Task<IDictionary<string, string>> InvokeHelperAsync(string args, IDictionary<string, string> standardInput);
1526
}
1627

1728
public class GitProcess : IGit
@@ -47,6 +58,55 @@ public Process CreateProcess(string args)
4758

4859
return new Process {StartInfo = psi};
4960
}
61+
62+
// This code was originally copied from
63+
// src/shared/Microsoft.Git.CredentialManager/Authentication/AuthenticationBase.cs
64+
// That code is for GUI helpers in this codebase, while the below is for
65+
// communicating over Git's stdin/stdout helper protocol. The GUI helper
66+
// protocol will one day use a different IPC mechanism, whereas this code
67+
// has to follow what upstream Git does.
68+
public async Task<IDictionary<string, string>> InvokeHelperAsync(string args, IDictionary<string, string> standardInput = null)
69+
{
70+
var procStartInfo = new ProcessStartInfo(_gitPath)
71+
{
72+
Arguments = args,
73+
RedirectStandardInput = true,
74+
RedirectStandardOutput = true,
75+
RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled
76+
UseShellExecute = false
77+
};
78+
79+
var process = Process.Start(procStartInfo);
80+
if (process is null)
81+
{
82+
throw new Exception($"Failed to start Git helper '{args}'");
83+
}
84+
85+
if (!(standardInput is null))
86+
{
87+
await process.StandardInput.WriteDictionaryAsync(standardInput);
88+
// some helpers won't continue until they see EOF
89+
// cf git-credential-cache
90+
process.StandardInput.Close();
91+
}
92+
93+
IDictionary<string, string> resultDict = await process.StandardOutput.ReadDictionaryAsync(StringComparer.OrdinalIgnoreCase);
94+
95+
await Task.Run(() => process.WaitForExit());
96+
int exitCode = process.ExitCode;
97+
98+
if (exitCode != 0)
99+
{
100+
if (!resultDict.TryGetValue("error", out string errorMessage))
101+
{
102+
errorMessage = "Unknown";
103+
}
104+
105+
throw new Exception($"helper error ({exitCode}): {errorMessage}");
106+
}
107+
108+
return resultDict;
109+
}
50110
}
51111

52112
public static class GitExtensions

0 commit comments

Comments
 (0)