Skip to content

Commit 21abaa0

Browse files
authored
Merge pull request #91 from mjcheetham/nointeractive
Add GCM_INTERACTIVE/credential.interactive setting
2 parents 7c26c5f + 5b2b752 commit 21abaa0

File tree

15 files changed

+363
-28
lines changed

15 files changed

+363
-28
lines changed

docs/configuration.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,37 @@ For the complete list of settings GCM Core understands, see the list below.
2222

2323
## Available settings
2424

25+
### credential.interactive
26+
27+
Permit or disable GCM Core from interacting with the user (showing GUI or TTY prompts). If interaction is required but has been disabled, an error is returned.
28+
29+
This can be helpful when using GCM Core in headless and unattended environments, such as build servers, where it would be preferable to fail than to hang indefinately waiting for a non-existent user.
30+
31+
To disable interactivity set this to `false` or `0`.
32+
33+
#### Compatibility
34+
35+
In previous versions of GCM this setting had a different behavior and accepted other values.
36+
The following table summarizes the change in behavior and the mapping of older values such as `never`:
37+
38+
Value(s)|Old meaning|New meaning
39+
-|-|-
40+
`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_
41+
`never`,<br/>`false`| Never prompt – fail if interaction is required|_(unchanged)_
42+
`always`,<br/>`force`,<br/>`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
43+
44+
#### Example
45+
46+
```shell
47+
git config --global credential.interactive false
48+
```
49+
50+
Defaults to enabled.
51+
52+
**Also see: [GCM_INTERACTIVE](environment.md#GCM_INTERACTIVE)**
53+
54+
---
55+
2556
### credential.provider
2657

2758
Define the host provider to use when authenticating.

docs/environment.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,45 @@ _No configuration equivalent._
121121

122122
---
123123

124+
### GCM_INTERACTIVE
125+
126+
Permit or disable GCM Core from interacting with the user (showing GUI or TTY prompts). If interaction is required but has been disabled, an error is returned.
127+
128+
This can be helpful when using GCM Core in headless and unattended environments, such as build servers, where it would be preferable to fail than to hang indefinately waiting for a non-existent user.
129+
130+
To disable interactivity set this to `false` or `0`.
131+
132+
#### Compatibility
133+
134+
In previous versions of GCM this setting had a different behavior and accepted other values.
135+
The following table summarizes the change in behavior and the mapping of older values such as `never`:
136+
137+
Value(s)|Old meaning|New meaning
138+
-|-|-
139+
`auto`|Prompt if required – use cached credentials if possible|_(unchanged)_
140+
`never`,<br/>`false`| Never prompt – fail if interaction is required|_(unchanged)_
141+
`always`,<br/>`force`,<br/>`true`|Always prompt – don't use cached credentials|Prompt if required (same as the old `auto` value)
142+
143+
#### Example
144+
145+
##### Windows
146+
147+
```batch
148+
SET GCM_INTERACTIVE=0
149+
```
150+
151+
##### macOS/Linux
152+
153+
```bash
154+
export GCM_INTERACTIVE=0
155+
```
156+
157+
Defaults to enabled.
158+
159+
**Also see: [credential.interactive](configuration.md#credentialinteractive)**
160+
161+
---
162+
124163
### GCM_PROVIDER
125164

126165
Define the host provider to use when authenticating.

src/osx/Microsoft.Authentication.Helper.Mac/Source/main.m

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ int main(int argc, const char * argv[]) {
2828

2929
@autoreleasepool {
3030
int exitCode;
31-
NSError* error;
31+
NSError *error;
32+
NSString *output;
3233

3334
AHLogger *logger = [[AHLogger alloc] init];
3435

@@ -97,24 +98,32 @@ int main(int argc, const char * argv[]) {
9798
NSString* clientId = [configs objectForKey:@"clientId"];
9899
NSString* resource = [configs objectForKey:@"resource"];
99100
NSString* redirectUri = [configs objectForKey:@"redirectUri"];
101+
NSString* interactive = [configs objectForKey:@"interactive"];
100102

101-
NSString *accessToken = [AHGenerateAccessToken generateAccessTokenWithAuthority:authority
102-
clientId:clientId
103-
resource:resource
104-
redirectUri:redirectUri
105-
error:&error
106-
logger:logger];
107-
108-
NSString* output;
109-
110-
if (error == nil && accessToken != nil)
103+
// We only perform interactive flows
104+
if (isTruthy(interactive))
111105
{
112-
output = [NSString stringWithFormat:@"accessToken=%@\n", accessToken];
113-
exitCode = 0;
106+
NSString *accessToken = [AHGenerateAccessToken generateAccessTokenWithAuthority:authority
107+
clientId:clientId
108+
resource:resource
109+
redirectUri:redirectUri
110+
error:&error
111+
logger:logger];
112+
113+
if (error == nil && accessToken != nil)
114+
{
115+
output = [NSString stringWithFormat:@"accessToken=%@\n", accessToken];
116+
exitCode = 0;
117+
}
118+
else
119+
{
120+
output = [NSString stringWithFormat:@"error=%@\n", [error description]];
121+
exitCode = -1;
122+
}
114123
}
115124
else
116125
{
117-
output = [NSString stringWithFormat:@"error=%@\n", [error description]];
126+
output = @"error=Interactivity is required but has been disabled.\n";
118127
exitCode = -1;
119128
}
120129

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public async Task<ICredential> GetCredentialsAsync(Uri targetUri)
3131
{
3232
string userName, password;
3333

34+
ThrowIfUserInteractionDisabled();
35+
3436
if (TryFindHelperExecutablePath(out string helperPath))
3537
{
3638
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, "--prompt userpass", null);
@@ -47,7 +49,7 @@ public async Task<ICredential> GetCredentialsAsync(Uri targetUri)
4749
}
4850
else
4951
{
50-
EnsureTerminalPromptsEnabled();
52+
ThrowIfTerminalPromptsDisabled();
5153

5254
Context.Terminal.WriteLine("Enter GitHub credentials for '{0}'...", targetUri);
5355

@@ -57,8 +59,11 @@ public async Task<ICredential> GetCredentialsAsync(Uri targetUri)
5759

5860
return new GitCredential(userName, password);
5961
}
62+
6063
public async Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms)
6164
{
65+
ThrowIfUserInteractionDisabled();
66+
6267
if (TryFindHelperExecutablePath(out string helperPath))
6368
{
6469
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, "--prompt authcode", null);
@@ -72,7 +77,7 @@ public async Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms)
7277
}
7378
else
7479
{
75-
EnsureTerminalPromptsEnabled();
80+
ThrowIfTerminalPromptsDisabled();
7681

7782
Context.Terminal.WriteLine("Two-factor authentication is enabled and an authentication code is required.");
7883

src/shared/Microsoft.Git.CredentialManager.Tests/Authentication/BasicAuthenticationTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ public void BasicAuthentication_GetCredentials_Resource_UserPassPromptReturnsCre
5555
Assert.Equal(testPassword, credential.Password);
5656
}
5757

58+
[Fact]
59+
public void BasicAuthentication_GetCredentials_NoInteraction_ThrowsException()
60+
{
61+
const string testResource = "https://example.com";
62+
63+
var context = new TestCommandContext
64+
{
65+
Settings = {IsInteractionAllowed = false},
66+
};
67+
68+
var basicAuth = new BasicAuthentication(context);
69+
70+
Assert.Throws<InvalidOperationException>(() => basicAuth.GetCredentials(testResource));
71+
}
72+
5873
[Fact]
5974
public void BasicAuthentication_GetCredentials_NoTerminalPrompts_ThrowsException()
6075
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using Microsoft.Git.CredentialManager.Authentication;
5+
using Microsoft.Git.CredentialManager.Tests.Objects;
6+
using Xunit;
7+
8+
namespace Microsoft.Git.CredentialManager.Tests.Authentication
9+
{
10+
public class MicrosoftAuthenticationTests
11+
{
12+
[Fact]
13+
public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenAsync_NoInteraction_ThrowsException()
14+
{
15+
const string authority = "https://login.microsoftonline.com/common";
16+
const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3";
17+
Uri redirectUri = new Uri("https://localhost");
18+
const string resource = "https://graph.microsoft.com";
19+
Uri remoteUri = new Uri("https://example.com");
20+
const string userName = null; // No user to ensure we do not use an existing token
21+
22+
var context = new TestCommandContext
23+
{
24+
Settings = {IsInteractionAllowed = false},
25+
};
26+
27+
var msAuth = new MicrosoftAuthentication(context);
28+
29+
await Assert.ThrowsAsync<InvalidOperationException>(
30+
() => msAuth.GetAccessTokenAsync(authority, clientId, redirectUri, resource, remoteUri, userName));
31+
}
32+
}
33+
}

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,135 @@ public void Settings_IsTerminalPromptsEnabled_EnvarFalsey_ReturnsFalse()
8787
Assert.False(settings.IsTerminalPromptsEnabled);
8888
}
8989

90+
[Fact]
91+
public void Settings_IsInteractionAllowed_EnvarUnset_ReturnsTrue()
92+
{
93+
var envars = new TestEnvironment();
94+
var git = new TestGit();
95+
96+
var settings = new Settings(envars, git);
97+
98+
Assert.True(settings.IsInteractionAllowed);
99+
}
100+
101+
[Fact]
102+
public void Settings_IsInteractionAllowed_EnvarTruthy_ReturnsTrue()
103+
{
104+
var envars = new TestEnvironment
105+
{
106+
Variables = {[Constants.EnvironmentVariables.GcmInteractive] = "1"}
107+
};
108+
var git = new TestGit();
109+
110+
var settings = new Settings(envars, git);
111+
112+
Assert.True(settings.IsInteractionAllowed);
113+
}
114+
115+
[Fact]
116+
public void Settings_IsInteractionAllowed_EnvarFalsey_ReturnsFalse()
117+
{
118+
var envars = new TestEnvironment
119+
{
120+
Variables = {[Constants.EnvironmentVariables.GcmInteractive] = "0"},
121+
};
122+
var git = new TestGit();
123+
124+
var settings = new Settings(envars, git);
125+
126+
Assert.False(settings.IsInteractionAllowed);
127+
}
128+
129+
[Fact]
130+
public void Settings_IsInteractionAllowed_ConfigAuto_ReturnsTrue()
131+
{
132+
const string section = Constants.GitConfiguration.Credential.SectionName;
133+
const string property = Constants.GitConfiguration.Credential.Interactive;
134+
135+
var envars = new TestEnvironment();
136+
var git = new TestGit();
137+
git.GlobalConfiguration[$"{section}.{property}"] = "auto";
138+
139+
var settings = new Settings(envars, git);
140+
141+
Assert.True(settings.IsInteractionAllowed);
142+
}
143+
144+
[Fact]
145+
public void Settings_IsInteractionAllowed_ConfigAlways_ReturnsTrue()
146+
{
147+
const string section = Constants.GitConfiguration.Credential.SectionName;
148+
const string property = Constants.GitConfiguration.Credential.Interactive;
149+
150+
var envars = new TestEnvironment();
151+
var git = new TestGit();
152+
git.GlobalConfiguration[$"{section}.{property}"] = "always";
153+
154+
var settings = new Settings(envars, git);
155+
156+
Assert.True(settings.IsInteractionAllowed);
157+
}
158+
159+
[Fact]
160+
public void Settings_IsInteractionAllowed_ConfigNever_ReturnsFalse()
161+
{
162+
const string section = Constants.GitConfiguration.Credential.SectionName;
163+
const string property = Constants.GitConfiguration.Credential.Interactive;
164+
165+
var envars = new TestEnvironment();
166+
var git = new TestGit();
167+
git.GlobalConfiguration[$"{section}.{property}"] = "never";
168+
169+
var settings = new Settings(envars, git);
170+
171+
Assert.False(settings.IsInteractionAllowed);
172+
}
173+
174+
[Fact]
175+
public void Settings_IsInteractionAllowed_ConfigTruthy_ReturnsTrue()
176+
{
177+
const string section = Constants.GitConfiguration.Credential.SectionName;
178+
const string property = Constants.GitConfiguration.Credential.Interactive;
179+
180+
var envars = new TestEnvironment();
181+
var git = new TestGit();
182+
git.GlobalConfiguration[$"{section}.{property}"] = "1";
183+
184+
var settings = new Settings(envars, git);
185+
186+
Assert.True(settings.IsInteractionAllowed);
187+
}
188+
189+
[Fact]
190+
public void Settings_IsInteractionAllowed_ConfigFalsey_ReturnsFalse()
191+
{
192+
const string section = Constants.GitConfiguration.Credential.SectionName;
193+
const string property = Constants.GitConfiguration.Credential.Interactive;
194+
195+
var envars = new TestEnvironment();
196+
var git = new TestGit();
197+
git.GlobalConfiguration[$"{section}.{property}"] = "0";
198+
199+
var settings = new Settings(envars, git);
200+
201+
Assert.False(settings.IsInteractionAllowed);
202+
}
203+
204+
[Fact]
205+
public void Settings_IsInteractionAllowed_ConfigNonBooleanyValue_ReturnsTrue()
206+
{
207+
const string section = Constants.GitConfiguration.Credential.SectionName;
208+
const string property = Constants.GitConfiguration.Credential.Interactive;
209+
210+
var envars = new TestEnvironment();
211+
var git = new TestGit();
212+
git.GlobalConfiguration[$"{section}.{property}"] = Guid.NewGuid().ToString();
213+
214+
var settings = new Settings(envars, git);
215+
216+
Assert.True(settings.IsInteractionAllowed);
217+
}
218+
90219
[Fact]
91220
public void Settings_IsTracingEnabled_EnvarUnset_ReturnsFalse()
92221
{

0 commit comments

Comments
 (0)