Skip to content

Commit

Permalink
Add GitHub Enterprise Server support (#286)
Browse files Browse the repository at this point in the history
* Explicitly disallow multiple accounts on GitHub extension

* Fix constructor bug

* Tested GHES

* Stashing

* Basic flow works

* Validation added

* Minor updates

* Minor Widget update

* SearchManager minor update

* Fixed LoginUI

* Created separate states for pages

* Added CredentialVault Tests

* Added LoginUI tests

* Added some tests

* Reverted Widget updates

* Fixed tests

* Revert changes to allow multi-user for tests

* PR Comments 1

* PR comments 2

* Ignore some tests in pipeline

* PR Comments 3

* PR Comments 4

* Minor update

* PR comments 6

* PR Comments 7
  • Loading branch information
vineeththomasalex authored Jan 11, 2024
1 parent 757d04b commit 82ae898
Show file tree
Hide file tree
Showing 41 changed files with 2,369 additions and 475 deletions.
13 changes: 10 additions & 3 deletions src/GitHubExtension/Client/GithubClientProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,16 @@ public async Task<GitHubClient> GetClientForLoggedInDeveloper(bool logRateLimit
}

if (logRateLimit)
{
var miscRateLimit = await client.RateLimit.GetRateLimits();
Log.Logger()?.ReportInfo($"Rate Limit: Remaining: {miscRateLimit.Resources.Core.Remaining} Total: {miscRateLimit.Resources.Core.Limit} Resets: {miscRateLimit.Resources.Core.Reset.ToStringInvariant()}");
{
try
{
var miscRateLimit = await client.RateLimit.GetRateLimits();
Log.Logger()?.ReportInfo($"Rate Limit: Remaining: {miscRateLimit.Resources.Core.Remaining} Total: {miscRateLimit.Resources.Core.Limit} Resets: {miscRateLimit.Resources.Core.Reset.ToStringInvariant()}");
}
catch (Exception ex)
{
Log.Logger()?.ReportError($"Rate limiting not enabled for server.", ex);
}
}

return client;
Expand Down
21 changes: 21 additions & 0 deletions src/GitHubExtension/Client/Validation.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation and Contributors
// Licensed under the MIT license.
using GitHubExtension.DataModel;
using Octokit;

namespace GitHubExtension.Client;

Expand Down Expand Up @@ -217,4 +218,24 @@ private static string AddProtocolToString(string s)

return n;
}

public static async Task<bool> IsReachableGitHubEnterpriseServerURL(Uri server)
{
try
{
var probeResult = await new EnterpriseProbe(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)).Probe(server);
if (probeResult != EnterpriseProbeResult.Ok)
{
Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} is not reachable");
return false;
}
}
catch (Exception ex)
{
Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} could not be probed.", ex);
return false;
}

return true;
}
}
1 change: 1 addition & 0 deletions src/GitHubExtension/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ internal class Constants
{
#pragma warning disable SA1310 // Field names should not contain underscore
public const string DEV_HOME_APPLICATION_NAME = "DevHome";
public const string GITHUB_COM_URL = "https://api.github.com/";
#pragma warning restore SA1310 // Field names should not contain underscore
}
6 changes: 3 additions & 3 deletions src/GitHubExtension/DataManager/GitHubSearchManger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

using GitHubExtension.Client;
using GitHubExtension.DataManager;
using GitHubExtension.DataModel;

using GitHubExtension.DataModel;

namespace GitHubExtension;

public delegate void SearchManagerResultsAvailableEventHandler(IEnumerable<Octokit.Issue> results, string resultType);
Expand All @@ -31,7 +31,7 @@ public GitHubSearchManager()
Environment.FailFast(e.Message, e);
return null;
}
}
}

public async Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/GitHubExtension/DataManager/IGitHubSearchManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license.

using GitHubExtension.DataManager;


namespace GitHubExtension;

public interface IGitHubSearchManager : IDisposable
Expand Down
1 change: 0 additions & 1 deletion src/GitHubExtension/DataModel/DataObjects/Issue.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation and Contributors
// Licensed under the MIT license.

using System.Globalization;
using Dapper;
using Dapper.Contrib.Extensions;
using GitHubExtension.Helpers;
Expand Down
98 changes: 67 additions & 31 deletions src/GitHubExtension/DeveloperId/CredentialVault.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,38 @@
// Licensed under the MIT license.

using System.ComponentModel;
using System.Net;
using System.Runtime.InteropServices;
using System.Security;
using Octokit;
using Windows.Security.Credentials;
using static GitHubExtension.DeveloperId.CredentialManager;

namespace GitHubExtension.DeveloperId;
internal static class CredentialVault
public class CredentialVault : ICredentialVault
{
private readonly string _credentialResourceName;

private static class CredentialVaultConfiguration
{
public const string CredResourceName = "GitHubDevHomeExtension";
}

internal static void SaveAccessTokenToVault(string loginId, SecureString? accessToken)
// Win32 Error codes
public const int Win32ErrorNotFound = 1168;

public CredentialVault(string applicationName = "")
{
_credentialResourceName = string.IsNullOrEmpty(applicationName) ? CredentialVaultConfiguration.CredResourceName : applicationName;
}

private string AddCredentialResourceNamePrefix(string loginId) => _credentialResourceName + ": " + loginId;

public void SaveCredentials(string loginId, SecureString? accessToken)
{
// Initialize a credential object.
var credential = new CREDENTIAL
{
Type = CRED_TYPE.GENERIC,
TargetName = CredentialVaultConfiguration.CredResourceName + ": " + loginId,
TargetName = AddCredentialResourceNamePrefix(loginId),
UserName = loginId,
Persist = (int)CRED_PERSIST.LocalMachine,
AttributeCount = 0,
Expand Down Expand Up @@ -61,29 +71,26 @@ internal static void SaveAccessTokenToVault(string loginId, SecureString? access
}
}

internal static void RemoveAccessTokenFromVault(string loginId)
public PasswordCredential? GetCredentials(string loginId)
{
var targetCredentialToDelete = CredentialVaultConfiguration.CredResourceName + ": " + loginId;
var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0);
if (!isCredentialDeleted)
{
Log.Logger()?.ReportInfo($"Deleting credentials from Credential Manager has failed");
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}

internal static PasswordCredential GetCredentialFromLocker(string loginId)
{
var credentialNameToRetrieve = CredentialVaultConfiguration.CredResourceName + ": " + loginId;
var credentialNameToRetrieve = AddCredentialResourceNamePrefix(loginId);
var ptrToCredential = IntPtr.Zero;

try
{
var isCredentialRetrieved = CredRead(credentialNameToRetrieve, CRED_TYPE.GENERIC, 0, out ptrToCredential);
if (!isCredentialRetrieved)
{
Log.Logger()?.ReportInfo($"Retrieving credentials from Credential Manager has failed");
throw new Win32Exception(Marshal.GetLastWin32Error());
var error = Marshal.GetLastWin32Error();
Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed for {loginId} with {error}");

// NotFound is expected and can be ignored.
if (error == Win32ErrorNotFound)
{
return null;
}

throw new Win32Exception(error);
}

CREDENTIAL credentialObject;
Expand All @@ -96,27 +103,30 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId)
}
else
{
Log.Logger()?.ReportInfo("No credentials found for this DeveloperId");
throw new ArgumentOutOfRangeException(loginId);
Log.Logger()?.ReportError($"No credentials found for this DeveloperId : {loginId}");
return null;
}

var accessTokenInChars = new char[credentialObject.CredentialBlobSize / 2];
Marshal.Copy(credentialObject.CredentialBlob, accessTokenInChars, 0, accessTokenInChars.Length);

var accessToken = new SecureString();
// convert accessTokenInChars to string
string accessTokenString = new (accessTokenInChars);

for (var i = 0; i < accessTokenInChars.Length; i++)
{
accessToken.AppendChar(accessTokenInChars[i]);

// Zero out characters after they are copied over from an unmanaged to managed type.
accessTokenInChars[i] = '\0';
}

accessToken.MakeReadOnly();

var credential = new PasswordCredential(CredentialVaultConfiguration.CredResourceName, loginId, new NetworkCredential(string.Empty, accessToken).Password);
var credential = new PasswordCredential(_credentialResourceName, loginId, accessTokenString);
return credential;
}
catch (Exception ex)
{
Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed unexpectedly: {loginId} : ", ex);
throw;
}
finally
{
if (ptrToCredential != IntPtr.Zero)
Expand All @@ -126,7 +136,17 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId)
}
}

public static IEnumerable<string> GetAllSavedLoginIds()
public void RemoveCredentials(string loginId)
{
var targetCredentialToDelete = AddCredentialResourceNamePrefix(loginId);
var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0);
if (!isCredentialDeleted)
{
Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed for {loginId}");
}
}

public IEnumerable<string> GetAllCredentials()
{
var ptrToCredential = IntPtr.Zero;

Expand All @@ -135,7 +155,7 @@ public static IEnumerable<string> GetAllSavedLoginIds()
IntPtr[] allCredentials;
uint count;

if (CredEnumerate(CredentialVaultConfiguration.CredResourceName + "*", 0, out count, out ptrToCredential) != false)
if (CredEnumerate(_credentialResourceName + "*", 0, out count, out ptrToCredential) != false)
{
allCredentials = new IntPtr[count];
Marshal.Copy(ptrToCredential, allCredentials, 0, (int)count);
Expand All @@ -145,7 +165,7 @@ public static IEnumerable<string> GetAllSavedLoginIds()
var error = Marshal.GetLastWin32Error();

// NotFound is expected and can be ignored.
if (error == 1168)
if (error == Win32ErrorNotFound)
{
return Enumerable.Empty<string>();
}
Expand Down Expand Up @@ -179,4 +199,20 @@ public static IEnumerable<string> GetAllSavedLoginIds()
}
}
}

public void RemoveAllCredentials()
{
var allCredentials = GetAllCredentials();
foreach (var credential in allCredentials)
{
try
{
RemoveCredentials(credential);
}
catch (Exception ex)
{
Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed unexpectedly: {credential} : ", ex);
}
}
}
}
5 changes: 3 additions & 2 deletions src/GitHubExtension/DeveloperId/DeveloperId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,16 @@ public Windows.Security.Credentials.PasswordCredential GetCredential(bool refres
return RefreshDeveloperId();
}

return CredentialVault.GetCredentialFromLocker(LoginId);
var credential = DeveloperIdProvider.GetInstance().GetCredentials(this) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId");
return credential;
}

public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId()
{
// Setting to MaxValue, since GitHub doesn't forcibly expire tokens currently.
CredentialExpiryTime = DateTime.MaxValue;
DeveloperIdProvider.GetInstance().RefreshDeveloperId(this);
var credential = CredentialVault.GetCredentialFromLocker(LoginId);
var credential = DeveloperIdProvider.GetInstance().GetCredentials(this) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId");
GitHubClient.Credentials = new (credential.Password);
return credential;
}
Expand Down
Loading

0 comments on commit 82ae898

Please sign in to comment.