Skip to content

Commit 0437eb5

Browse files
committed
ui: correctly check for GUI on Win/Mac/POSIX
Replace the `PlatformUtils.IsDesktopSession` util method with platform specific components that check use native APIs to determine the state of the current session. `Environment.UserInteractive` is hard-coded to return true for POSIX and Windows platforms on .NET Core 2.x and 3.x. dotnet/runtime#770 The .NET 5 implementation (not yet released) fixes this for the Windows platform only. We take a copy of that implementation for Windows. On macOS we use the `SessionGetInfo` from the Security.framework. On POSIX in general we also check for the X11 $DISPLAY environment variable.
1 parent 4d0e394 commit 0437eb5

File tree

17 files changed

+237
-29
lines changed

17 files changed

+237
-29
lines changed

NOTICE

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Do Not Translate or Localize
44
This repository for Git Credential Manager Core includes material from the
55
projects listed below.
66

7+
--------------------------------------------------------------------------------
78
1. GitHub/VisualStudio (https://github.com/github/VisualStudio)
89

910
Copyright (c) GitHub Inc.
@@ -24,3 +25,30 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
2425
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
2526
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
2627
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28+
29+
--------------------------------------------------------------------------------
30+
2. dotnet/runtime (https://github.com/dotnet/runtime)
31+
32+
The MIT License (MIT)
33+
34+
Copyright (c) .NET Foundation and Contributors
35+
36+
All rights reserved.
37+
38+
Permission is hereby granted, free of charge, to any person obtaining a copy
39+
of this software and associated documentation files (the "Software"), to deal
40+
in the Software without restriction, including without limitation the rights
41+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
42+
copies of the Software, and to permit persons to whom the Software is
43+
furnished to do so, subject to the following conditions:
44+
45+
The above copyright notice and this permission notice shall be included in all
46+
copies or substantial portions of the Software.
47+
48+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
51+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
52+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
53+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
54+
SOFTWARE.

src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public async Task<ICredential> GetBasicCredentialsAsync(Uri targetUri, string us
4545
string password;
4646

4747
// Shell out to the UI helper and show the Bitbucket u/p prompt
48-
if (Context.IsDesktopSession && TryFindHelperExecutablePath(out string helperPath))
48+
if (Context.SessionManager.IsDesktopSession && TryFindHelperExecutablePath(out string helperPath))
4949
{
5050
var cmdArgs = new StringBuilder("--prompt userpass");
5151
if (!string.IsNullOrWhiteSpace(userName))
@@ -96,7 +96,7 @@ public async Task<bool> ShowOAuthRequiredPromptAsync()
9696
ThrowIfUserInteractionDisabled();
9797

9898
// Shell out to the UI helper and show the Bitbucket prompt
99-
if (Context.IsDesktopSession && TryFindHelperExecutablePath(out string helperPath))
99+
if (Context.SessionManager.IsDesktopSession && TryFindHelperExecutablePath(out string helperPath))
100100
{
101101
IDictionary<string, string> output = await InvokeHelperAsync(helperPath, "--prompt oauth");
102102

src/shared/Git-Credential-Manager/NOTICE

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Notwithstanding any other terms, you may reverse engineer this software to the
1616
extent required to debug changes to any libraries licensed under the GNU Lesser
1717
General Public License.
1818

19+
--------------------------------------------------------------------------------
1920
1. GitHub/VisualStudio (https://github.com/github/VisualStudio)
2021

2122
Copyright (c) GitHub Inc.
@@ -36,3 +37,30 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
3637
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
3738
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
3839
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40+
41+
--------------------------------------------------------------------------------
42+
2. dotnet/runtime (https://github.com/dotnet/runtime)
43+
44+
The MIT License (MIT)
45+
46+
Copyright (c) .NET Foundation and Contributors
47+
48+
All rights reserved.
49+
50+
Permission is hereby granted, free of charge, to any person obtaining a copy
51+
of this software and associated documentation files (the "Software"), to deal
52+
in the Software without restriction, including without limitation the rights
53+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
54+
copies of the Software, and to permit persons to whom the Software is
55+
furnished to do so, subject to the following conditions:
56+
57+
The above copyright notice and this permission notice shall be included in all
58+
copies or substantial portions of the Software.
59+
60+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
63+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
64+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
65+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
66+
SOFTWARE.

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ public async Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetU
6363

6464
// If the GitHub auth stack doesn't support flows such as RFC 8628 and we do not have
6565
// an interactive desktop session, we cannot offer OAuth authentication.
66-
if ((modes & AuthenticationModes.OAuth) != 0 && !Context.IsDesktopSession && !GitHubConstants.IsOAuthDeviceAuthSupported)
66+
if ((modes & AuthenticationModes.OAuth) != 0
67+
&& !Context.SessionManager.IsDesktopSession
68+
&& !GitHubConstants.IsOAuthDeviceAuthSupported)
6769
{
6870
Context.Trace.WriteLine("Ignoring OAuth authentication mode because we are not in an interactive desktop session. GitHub does not support RFC 8628.");
6971

@@ -189,7 +191,7 @@ public async Task<OAuth2TokenResult> GetOAuthTokenAsync(Uri targetUri, IEnumerab
189191
var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri);
190192

191193
// If we have a desktop session try authentication using the user's default web browser
192-
if (Context.IsDesktopSession)
194+
if (Context.SessionManager.IsDesktopSession)
193195
{
194196
var browserOptions = new OAuth2WebBrowserOptions
195197
{

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUser
2626
const string testUserName = "john.doe";
2727
const string testPassword = "letmein123";
2828

29-
var context = new TestCommandContext {IsDesktopSession = false};
29+
var context = new TestCommandContext {SessionManager = {IsDesktopSession = false}};
3030
context.Terminal.SecretPrompts["Password"] = testPassword;
3131

3232
var basicAuth = new BasicAuthentication(context);
@@ -44,7 +44,7 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPa
4444
const string testUserName = "john.doe";
4545
const string testPassword = "letmein123";
4646

47-
var context = new TestCommandContext {IsDesktopSession = false};
47+
var context = new TestCommandContext {SessionManager = {IsDesktopSession = false}};
4848
context.Terminal.Prompts["Username"] = testUserName;
4949
context.Terminal.SecretPrompts["Password"] = testPassword;
5050

@@ -63,7 +63,7 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPromp
6363

6464
var context = new TestCommandContext
6565
{
66-
IsDesktopSession = false,
66+
SessionManager = {IsDesktopSession = false},
6767
Settings = {IsInteractionAllowed = false},
6868
};
6969

@@ -81,7 +81,7 @@ public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassP
8181

8282
var context = new TestCommandContext
8383
{
84-
IsDesktopSession = true,
84+
SessionManager = {IsDesktopSession = true},
8585
SystemPrompts =
8686
{
8787
CredentialPrompt = (resource, userName) =>
@@ -112,7 +112,7 @@ public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_Pa
112112

113113
var context = new TestCommandContext
114114
{
115-
IsDesktopSession = true,
115+
SessionManager = {IsDesktopSession = true},
116116
SystemPrompts =
117117
{
118118
CredentialPrompt = (resource, userName) =>
@@ -144,7 +144,7 @@ public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_Pa
144144

145145
var context = new TestCommandContext
146146
{
147-
IsDesktopSession = true,
147+
SessionManager = {IsDesktopSession = true},
148148
SystemPrompts =
149149
{
150150
CredentialPrompt = (resource, userName) =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public ICredential GetCredentials(string resource, string userName)
3434
ThrowIfUserInteractionDisabled();
3535

3636
// TODO: we only support system GUI prompts on Windows currently
37-
if (Context.IsDesktopSession && PlatformUtils.IsWindows())
37+
if (Context.SessionManager.IsDesktopSession && PlatformUtils.IsWindows())
3838
{
3939
return GetCredentialsByUi(resource, userName);
4040
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
119119
{
120120
#if NETFRAMEWORK
121121
// If we're in an interactive session and on .NET Framework, let MSAL show the WinForms-based embeded UI
122-
if (PlatformUtils.IsDesktopSession())
122+
if (Context.SessionManager.IsDesktopSession)
123123
{
124124
result = await app.AcquireTokenInteractive(scopes)
125125
.WithPrompt(Prompt.SelectAccount)
@@ -128,7 +128,7 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
128128
}
129129
#elif NETSTANDARD
130130
// MSAL requires the application redirect URI is a loopback address to use the System WebView
131-
if (Context.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
131+
if (Context.SessionManager.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
132132
{
133133
result = await app.AcquireTokenInteractive(scopes)
134134
.WithPrompt(Prompt.SelectAccount)

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ public interface ICommandContext : IDisposable
2929
ITerminal Terminal { get; }
3030

3131
/// <summary>
32-
/// Returns true if in a GUI session/desktop is available, false otherwise.
32+
/// Provides services regarding user sessions.
3333
/// </summary>
34-
bool IsDesktopSession { get; }
34+
ISessionManager SessionManager { get; }
3535

3636
/// <summary>
3737
/// Application tracing system.
@@ -85,6 +85,7 @@ public CommandContext()
8585
FileSystem = new WindowsFileSystem();
8686
Environment = new WindowsEnvironment(FileSystem);
8787
Terminal = new WindowsTerminal(Trace);
88+
SessionManager = new WindowsSessionManager();
8889
CredentialStore = WindowsCredentialManager.Open();
8990
SystemPrompts = new WindowsSystemPrompts();
9091
}
@@ -93,6 +94,7 @@ public CommandContext()
9394
if (PlatformUtils.IsMacOS())
9495
{
9596
FileSystem = new MacOSFileSystem();
97+
SessionManager = new MacOSSessionManager();
9698
CredentialStore = MacOSKeychain.Open();
9799
SystemPrompts = new MacOSSystemPrompts();
98100
}
@@ -108,7 +110,6 @@ public CommandContext()
108110
string repoPath = Git.GetRepositoryPath(FileSystem.GetCurrentDirectory());
109111
Settings = new Settings(Environment, Git, repoPath);
110112
HttpClientFactory = new HttpClientFactory(Trace, Settings, Streams);
111-
IsDesktopSession = PlatformUtils.IsDesktopSession();
112113

113114
// Set the parent window handle/ID
114115
SystemPrompts.ParentWindowId = Settings.ParentWindowId;
@@ -122,7 +123,7 @@ public CommandContext()
122123

123124
public ITerminal Terminal { get; }
124125

125-
public bool IsDesktopSession { get; }
126+
public ISessionManager SessionManager { get; }
126127

127128
public ITrace Trace { get; }
128129

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Microsoft.Git.CredentialManager
2+
{
3+
public interface ISessionManager
4+
{
5+
/// <summary>
6+
/// Determine if the current session has access to a desktop/can display UI.
7+
/// </summary>
8+
/// <returns>True if the session can display UI, false otherwise.</returns>
9+
bool IsDesktopSession { get; }
10+
}
11+
}
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 Microsoft.Git.CredentialManager.Interop.MacOS.Native;
4+
using Microsoft.Git.CredentialManager.Interop.Posix;
5+
6+
namespace Microsoft.Git.CredentialManager.Interop.MacOS
7+
{
8+
public class MacOSSessionManager : PosixSessionManager
9+
{
10+
public MacOSSessionManager()
11+
{
12+
PlatformUtils.EnsureMacOS();
13+
}
14+
15+
public override bool IsDesktopSession
16+
{
17+
get
18+
{
19+
// Get information about the current session
20+
int error = SecurityFramework.SessionGetInfo(SecurityFramework.CallerSecuritySession, out int id, out var sessionFlags);
21+
22+
// Check if the session supports Quartz
23+
if (error == 0 && (sessionFlags & SessionAttributeBits.SessionHasGraphicAccess) != 0)
24+
{
25+
return true;
26+
}
27+
28+
// Fall-through and check if X11 is available on macOS
29+
return base.IsDesktopSession;
30+
}
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)