Skip to content

Commit abb5dd3

Browse files
committed
gitlab: invoke GitLab UI helper if available
Invoke the GitLab UI helper if the current session is GUI interactive, the user has not disabled GUI prompts, and the helper is found.
1 parent e09b656 commit abb5dd3

File tree

4 files changed

+158
-86
lines changed

4 files changed

+158
-86
lines changed

src/shared/GitLab.Tests/GitLabAuthenticationTests.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,89 +8,89 @@ namespace GitLab.Tests
88
public class GitLabAuthenticationTests
99
{
1010
[Fact]
11-
public void GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesNone_ThrowsException()
11+
public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesNone_ThrowsException()
1212
{
1313
var context = new TestCommandContext();
1414
var auth = new GitLabAuthentication(context);
15-
Assert.Throws<ArgumentException>("modes",
16-
() => auth.GetAuthentication(null, null, AuthenticationModes.None)
15+
await Assert.ThrowsAsync<ArgumentException>("modes",
16+
() => auth.GetAuthenticationAsync(null, null, AuthenticationModes.None)
1717
);
1818
}
1919

2020
[Theory]
2121
[InlineData(AuthenticationModes.Browser)]
22-
public void GitLabAuthentication_GetAuthenticationAsync_SingleChoice_TerminalAndInteractionNotRequired(GitLab.AuthenticationModes modes)
22+
public async Task GitLabAuthentication_GetAuthenticationAsync_SingleChoice_TerminalAndInteractionNotRequired(GitLab.AuthenticationModes modes)
2323
{
2424
var context = new TestCommandContext();
2525
context.Settings.IsTerminalPromptsEnabled = false;
2626
context.Settings.IsInteractionAllowed = false;
2727
context.SessionManager.IsDesktopSession = true; // necessary for browser
2828
var auth = new GitLabAuthentication(context);
29-
var result = auth.GetAuthentication(null, null, modes);
29+
var result = await auth.GetAuthenticationAsync(null, null, modes);
3030
Assert.Equal(modes, result.AuthenticationMode);
3131
}
3232

3333
[Fact]
34-
public void GitLabAuthentication_GetAuthenticationAsync_TerminalPromptsDisabled_Throws()
34+
public async Task GitLabAuthentication_GetAuthenticationAsync_TerminalPromptsDisabled_Throws()
3535
{
3636
var context = new TestCommandContext();
3737
context.Settings.IsTerminalPromptsEnabled = false;
3838
var auth = new GitLabAuthentication(context);
39-
var exception = Assert.Throws<InvalidOperationException>(
40-
() => auth.GetAuthentication(null, null, AuthenticationModes.All)
39+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
40+
() => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All)
4141
);
4242
Assert.Equal("Cannot prompt because terminal prompts have been disabled.", exception.Message);
4343
}
4444

4545
[Fact]
46-
public void GitLabAuthentication_GetAuthenticationAsync_Terminal()
46+
public async Task GitLabAuthentication_GetAuthenticationAsync_Terminal()
4747
{
4848
var context = new TestCommandContext();
4949
var auth = new GitLabAuthentication(context);
5050
context.SessionManager.IsDesktopSession = true;
5151
context.Terminal.Prompts["option (enter for default)"] = "";
52-
var result = auth.GetAuthentication(null, null, AuthenticationModes.All);
52+
var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);
5353
Assert.Equal(AuthenticationModes.Browser, result.AuthenticationMode);
5454
}
5555

5656
[Fact]
57-
public void GitLabAuthentication_GetAuthenticationAsync_ChoosePat()
57+
public async Task GitLabAuthentication_GetAuthenticationAsync_ChoosePat()
5858
{
5959
var context = new TestCommandContext();
6060
var auth = new GitLabAuthentication(context);
6161
context.Terminal.Prompts["option (enter for default)"] = "";
6262
context.Terminal.Prompts["Username"] = "username";
6363
context.Terminal.SecretPrompts["Personal access token"] = "token";
64-
var result = auth.GetAuthentication(null, null, AuthenticationModes.All);
64+
var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);
6565
Assert.Equal(AuthenticationModes.Pat, result.AuthenticationMode);
6666
Assert.Equal("username", result.Credential.Account);
6767
Assert.Equal("token", result.Credential.Password);
6868
}
6969

7070
[Fact]
71-
public void GitLabAuthentication_GetAuthenticationAsync_ChooseBasic()
71+
public async Task GitLabAuthentication_GetAuthenticationAsync_ChooseBasic()
7272
{
7373
var context = new TestCommandContext();
7474
var auth = new GitLabAuthentication(context);
7575
context.Terminal.Prompts["option (enter for default)"] = "2";
7676
context.Terminal.Prompts["Username"] = "username";
7777
context.Terminal.SecretPrompts["Password"] = "password";
78-
var result = auth.GetAuthentication(null, null, AuthenticationModes.All);
78+
var result = await auth.GetAuthenticationAsync(null, null, AuthenticationModes.All);
7979
Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
8080
Assert.Equal("username", result.Credential.Account);
8181
Assert.Equal("password", result.Credential.Password);
8282
}
8383

8484
[Fact]
85-
public void GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesAll_RequiresInteraction()
85+
public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationModesAll_RequiresInteraction()
8686
{
8787
var context = new TestCommandContext();
8888
context.Settings.IsInteractionAllowed = false;
8989
var auth = new GitLabAuthentication(context);
90-
var exception = Assert.Throws<InvalidOperationException>(
91-
() => auth.GetAuthentication(new Uri("https://GitLab.com"), null, AuthenticationModes.All)
90+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
91+
() => auth.GetAuthenticationAsync(new Uri("https://GitLab.com"), null, AuthenticationModes.All)
9292
);
9393
Assert.Equal("Cannot prompt because user interactivity has been disabled.", exception.Message);
9494
}
9595
}
96-
}
96+
}

src/shared/GitLab/GitLabAuthentication.cs

Lines changed: 134 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace GitLab
1212
{
1313
public interface IGitLabAuthentication : IDisposable
1414
{
15-
AuthenticationPromptResult GetAuthentication(Uri targetUri, string userName, AuthenticationModes modes);
15+
Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes);
1616

1717
Task<OAuth2TokenResult> GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable<string> scopes);
1818

@@ -53,7 +53,7 @@ public class GitLabAuthentication : AuthenticationBase, IGitLabAuthentication
5353
public GitLabAuthentication(ICommandContext context)
5454
: base(context) { }
5555

56-
public AuthenticationPromptResult GetAuthentication(Uri targetUri, string userName, AuthenticationModes modes)
56+
public async Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes)
5757
{
5858
// If we don't have a desktop session/GUI then we cannot offer browser
5959
if (!Context.SessionManager.IsDesktopSession)
@@ -67,70 +67,130 @@ public AuthenticationPromptResult GetAuthentication(Uri targetUri, string userNa
6767
throw new ArgumentException(@$"Must specify at least one {nameof(AuthenticationModes)}", nameof(modes));
6868
}
6969

70-
switch (modes)
70+
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
71+
TryFindHelperExecutablePath(out string helperPath))
7172
{
72-
case AuthenticationModes.Basic:
73-
ThrowIfUserInteractionDisabled();
74-
ThrowIfTerminalPromptsDisabled();
75-
Context.Terminal.WriteLine("Enter GitLab credentials for '{0}'...", targetUri);
76-
77-
if (string.IsNullOrWhiteSpace(userName))
78-
{
79-
userName = Context.Terminal.Prompt("Username");
80-
}
81-
else
82-
{
83-
Context.Terminal.WriteLine("Username: {0}", userName);
84-
}
85-
86-
string password = Context.Terminal.PromptSecret("Password");
87-
return new AuthenticationPromptResult(AuthenticationModes.Basic, new GitCredential(userName, password));
88-
89-
case AuthenticationModes.Pat:
90-
ThrowIfUserInteractionDisabled();
91-
ThrowIfTerminalPromptsDisabled();
92-
Context.Terminal.WriteLine("Enter GitLab credentials for '{0}'...", targetUri);
93-
94-
if (string.IsNullOrWhiteSpace(userName))
95-
{
96-
userName = Context.Terminal.Prompt("Username");
97-
}
98-
else
99-
{
100-
Context.Terminal.WriteLine("Username: {0}", userName);
101-
}
102-
103-
string token = Context.Terminal.PromptSecret("Personal access token");
104-
return new AuthenticationPromptResult(AuthenticationModes.Pat, new GitCredential(userName, token));
105-
106-
case AuthenticationModes.Browser:
107-
return new AuthenticationPromptResult(AuthenticationModes.Browser);
108-
109-
case AuthenticationModes.None:
110-
throw new ArgumentOutOfRangeException(nameof(modes), @$"At least one {nameof(AuthenticationModes)} must be supplied");
111-
112-
default:
113-
ThrowIfUserInteractionDisabled();
114-
ThrowIfTerminalPromptsDisabled();
115-
var menuTitle = $"Select an authentication method for '{targetUri}'";
116-
var menu = new TerminalMenu(Context.Terminal, menuTitle);
117-
118-
TerminalMenuItem browserItem = null;
119-
TerminalMenuItem basicItem = null;
120-
TerminalMenuItem patItem = null;
121-
122-
if ((modes & AuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
123-
if ((modes & AuthenticationModes.Pat) != 0) patItem = menu.Add("Personal access token");
124-
if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
125-
126-
// Default to the 'first' choice in the menu
127-
TerminalMenuItem choice = menu.Show(0);
128-
129-
if (choice == browserItem) goto case AuthenticationModes.Browser;
130-
if (choice == basicItem) goto case AuthenticationModes.Basic;
131-
if (choice == patItem) goto case AuthenticationModes.Pat;
132-
133-
throw new Exception();
73+
var cmdArgs = new StringBuilder("prompt");
74+
if (!string.IsNullOrWhiteSpace(userName))
75+
{
76+
cmdArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
77+
}
78+
79+
if ((modes & AuthenticationModes.Basic) != 0) cmdArgs.Append(" --basic");
80+
if ((modes & AuthenticationModes.Browser) != 0) cmdArgs.Append(" --browser");
81+
if ((modes & AuthenticationModes.Pat) != 0) cmdArgs.Append(" --pat");
82+
83+
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, cmdArgs.ToString());
84+
85+
if (!resultDict.TryGetValue("mode", out string responseMode))
86+
{
87+
throw new Exception("Missing 'mode' in response");
88+
}
89+
90+
switch (responseMode.ToLowerInvariant())
91+
{
92+
case "pat":
93+
if (!resultDict.TryGetValue("pat", out string pat))
94+
{
95+
throw new Exception("Missing 'pat' in response");
96+
}
97+
98+
if (!resultDict.TryGetValue("username", out string patUserName))
99+
{
100+
// Username is optional for PATs
101+
}
102+
103+
return new AuthenticationPromptResult(
104+
AuthenticationModes.Pat, new GitCredential(patUserName, pat));
105+
106+
case "browser":
107+
return new AuthenticationPromptResult(AuthenticationModes.Browser);
108+
109+
case "basic":
110+
if (!resultDict.TryGetValue("username", out userName))
111+
{
112+
throw new Exception("Missing 'username' in response");
113+
}
114+
115+
if (!resultDict.TryGetValue("password", out string password))
116+
{
117+
throw new Exception("Missing 'password' in response");
118+
}
119+
120+
return new AuthenticationPromptResult(
121+
AuthenticationModes.Basic, new GitCredential(userName, password));
122+
123+
default:
124+
throw new Exception($"Unknown mode value in response '{responseMode}'");
125+
}
126+
}
127+
else
128+
{
129+
switch (modes)
130+
{
131+
case AuthenticationModes.Basic:
132+
ThrowIfUserInteractionDisabled();
133+
ThrowIfTerminalPromptsDisabled();
134+
Context.Terminal.WriteLine("Enter GitLab credentials for '{0}'...", targetUri);
135+
136+
if (string.IsNullOrWhiteSpace(userName))
137+
{
138+
userName = Context.Terminal.Prompt("Username");
139+
}
140+
else
141+
{
142+
Context.Terminal.WriteLine("Username: {0}", userName);
143+
}
144+
145+
string password = Context.Terminal.PromptSecret("Password");
146+
return new AuthenticationPromptResult(AuthenticationModes.Basic, new GitCredential(userName, password));
147+
148+
case AuthenticationModes.Pat:
149+
ThrowIfUserInteractionDisabled();
150+
ThrowIfTerminalPromptsDisabled();
151+
Context.Terminal.WriteLine("Enter GitLab credentials for '{0}'...", targetUri);
152+
153+
if (string.IsNullOrWhiteSpace(userName))
154+
{
155+
userName = Context.Terminal.Prompt("Username");
156+
}
157+
else
158+
{
159+
Context.Terminal.WriteLine("Username: {0}", userName);
160+
}
161+
162+
string token = Context.Terminal.PromptSecret("Personal access token");
163+
return new AuthenticationPromptResult(AuthenticationModes.Pat, new GitCredential(userName, token));
164+
165+
case AuthenticationModes.Browser:
166+
return new AuthenticationPromptResult(AuthenticationModes.Browser);
167+
168+
case AuthenticationModes.None:
169+
throw new ArgumentOutOfRangeException(nameof(modes), @$"At least one {nameof(AuthenticationModes)} must be supplied");
170+
171+
default:
172+
ThrowIfUserInteractionDisabled();
173+
ThrowIfTerminalPromptsDisabled();
174+
var menuTitle = $"Select an authentication method for '{targetUri}'";
175+
var menu = new TerminalMenu(Context.Terminal, menuTitle);
176+
177+
TerminalMenuItem browserItem = null;
178+
TerminalMenuItem basicItem = null;
179+
TerminalMenuItem patItem = null;
180+
181+
if ((modes & AuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
182+
if ((modes & AuthenticationModes.Pat) != 0) patItem = menu.Add("Personal access token");
183+
if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
184+
185+
// Default to the 'first' choice in the menu
186+
TerminalMenuItem choice = menu.Show(0);
187+
188+
if (choice == browserItem) goto case AuthenticationModes.Browser;
189+
if (choice == basicItem) goto case AuthenticationModes.Basic;
190+
if (choice == patItem) goto case AuthenticationModes.Pat;
191+
192+
throw new Exception();
193+
}
134194
}
135195
}
136196

@@ -164,6 +224,15 @@ public async Task<OAuth2TokenResult> GetOAuthTokenViaRefresh(Uri targetUri, stri
164224
return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None);
165225
}
166226

227+
private bool TryFindHelperExecutablePath(out string path)
228+
{
229+
return TryFindHelperExecutablePath(
230+
GitLabConstants.EnvironmentVariables.AuthenticationHelper,
231+
GitLabConstants.GitConfiguration.Credential.AuthenticationHelper,
232+
GitLabConstants.DefaultAuthenticationHelper,
233+
out path);
234+
}
235+
167236
private HttpClient _httpClient;
168237
private HttpClient HttpClient => _httpClient ?? (_httpClient = Context.HttpClientFactory.CreateClient());
169238

src/shared/GitLab/GitLabConstants.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ public static class GitLabConstants
66
{
77
public static readonly Uri GitLabDotCom = new Uri("https://gitlab.com");
88

9-
// owned by https://gitlab.com/gitcredentialmanager
9+
public const string DefaultAuthenticationHelper = "GitLab.UI";
10+
11+
// Owned by https://gitlab.com/gitcredentialmanager
1012
public const string OAuthClientId = "172b9f227872b5dde33f4d9b1db06a6a5515ae79508e7a00c973c85ce490671e";
1113
public const string OAuthClientSecret = "7da92770d1447508601e4ba026bc5eb655c8268e818cd609889cc9bae2023f39";
1214

@@ -23,7 +25,7 @@ public static class EnvironmentVariables
2325
public const string DevOAuthClientSecret = "GCM_DEV_GITLAB_CLIENTSECRET";
2426
public const string DevOAuthRedirectUri = "GCM_DEV_GITLAB_REDIRECTURI";
2527
public const string AuthenticationModes = "GCM_GITLAB_AUTHMODES";
26-
28+
public const string AuthenticationHelper = "GCM_GITLAB_HELPER";
2729
}
2830

2931
public static class GitConfiguration
@@ -34,6 +36,7 @@ public static class Credential
3436
public const string DevOAuthClientId = "gitLabDevClientId";
3537
public const string DevOAuthClientSecret = "gitLabDevClientSecret";
3638
public const string DevOAuthRedirectUri = "gitLabDevRedirectUri";
39+
public const string AuthenticationHelper = "gitLabHelper";
3740
}
3841
}
3942

src/shared/GitLab/GitLabHostProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
9797

9898
AuthenticationModes authModes = GetSupportedAuthenticationModes(remoteUri);
9999

100-
AuthenticationPromptResult promptResult = _gitLabAuth.GetAuthentication(remoteUri, input.UserName, authModes);
100+
AuthenticationPromptResult promptResult = await _gitLabAuth.GetAuthenticationAsync(remoteUri, input.UserName, authModes);
101101

102102
switch (promptResult.AuthenticationMode)
103103
{

0 commit comments

Comments
 (0)