From 594662262249b60d715b042bb6cc77d2971c0cda Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 31 Jul 2024 17:03:10 +0200 Subject: [PATCH] feat: OAuth2 option [IDE-461] [IDE-465] (#288) --- .github/workflows/integration-tests.yml | 1 - CHANGELOG.md | 5 ++ .../Snyk.Code.Library.Tests.csproj | 28 +++---- .../SnykCode/Api/SnykCodeClientTest.cs | 41 ++++----- .../SnykCode/TestSettings.cs | 9 +- .../Service/ApiEndpointResolverTest.cs | 43 ++++++++-- .../Service/SnykApiServiceTest.cs | 20 ++--- Snyk.Common.Tests/Snyk.Common.Tests.csproj | 14 +++- .../Authentication/AuthenticationToken.cs | 19 +++-- .../Authentication/AuthenticationType.cs | 6 +- Snyk.Common/HttpClientFactory.cs | 2 +- Snyk.Common/Service/ApiEndpointResolver.cs | 29 +++---- Snyk.Common/Settings/ISnykOptions.cs | 29 ++++--- .../CLI/SnykCli.cs | 47 ++++++----- .../Commands/AbstractTaskCommand.cs | 2 +- .../Commands/SnykCleanPanelCommand.cs | 2 +- .../Settings/SnykGeneralOptionsDialogPage.cs | 58 +++++++++++-- ...SnykGeneralSettingsUserControl.Designer.cs | 83 ++++++++++++++----- .../SnykGeneralSettingsUserControl.cs | 65 +++++++++++---- .../SnykGeneralSettingsUserControl.resx | 25 ++---- .../Settings/SnykSettings.cs | 9 +- .../SnykUserStorageSettingsService.cs | 28 +++++-- .../Snyk.VisualStudio.Extension.2022.csproj | 1 + .../SnykVSPackage.cs | 1 - .../UI/Notifications/VsInfoBarService.cs | 8 +- .../SnykCliTest.cs | 2 +- .../UI/ResourceLoaderTest.cs | 9 +- 27 files changed, 374 insertions(+), 212 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 24a014886..836864ac2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -9,7 +9,6 @@ on: jobs: test-vs22: - continue-on-error: true runs-on: windows-2022 defaults: run: diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f3f1f83..3573abdd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Snyk Security Changelog +## [1.1.62] + +### Fixed +- Added OAuth2 authentication Option to settings window. + ## [1.1.61] ### Fixed diff --git a/Snyk.Code.Library.Tests/Snyk.Code.Library.Tests.csproj b/Snyk.Code.Library.Tests/Snyk.Code.Library.Tests.csproj index 8e6f5645f..2ed959006 100644 --- a/Snyk.Code.Library.Tests/Snyk.Code.Library.Tests.csproj +++ b/Snyk.Code.Library.Tests/Snyk.Code.Library.Tests.csproj @@ -54,28 +54,17 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -86,7 +75,10 @@ - + + true + true + diff --git a/Snyk.Code.Library.Tests/SnykCode/Api/SnykCodeClientTest.cs b/Snyk.Code.Library.Tests/SnykCode/Api/SnykCodeClientTest.cs index 3f27f71a1..5d8de6b5b 100644 --- a/Snyk.Code.Library.Tests/SnykCode/Api/SnykCodeClientTest.cs +++ b/Snyk.Code.Library.Tests/SnykCode/Api/SnykCodeClientTest.cs @@ -1,35 +1,28 @@ -namespace Snyk.Code.Library.Tests.SnykCode.Api +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Snyk.Code.Library.Api; +using Snyk.Code.Library.Api.Dto; +using Snyk.Code.Library.Api.Dto.Analysis; +using Snyk.Common; +using Snyk.Common.Authentication; +using Xunit; + +namespace Snyk.Code.Library.Tests.SnykCode.Api { - using System; - using System.Collections.Generic; - using System.IO; - using System.Threading.Tasks; - using Snyk.Code.Library.Api; - using Snyk.Code.Library.Api.Dto; - using Snyk.Code.Library.Api.Dto.Analysis; - using Snyk.Code.Library.Tests.Api; - using Snyk.Common; - using Snyk.Common.Authentication; - using Xunit; - /// /// Tests for . /// public class SnykCodeClientTest { private const string ContextFlowName = "test-visual-studio-plugin-ide"; - private const string ContextOrgName = "platform_hammerhead"; - - private SnykCodeClient snykCodeClient; + private const string ContextOrgName = "devex_ide"; - public SnykCodeClientTest() - { - this.snykCodeClient = new SnykCodeClient( - TestSettings.SnykCodeApiUrl, - TestSettings.Instance.ApiToken, - ContextFlowName, - ContextOrgName); - } + private readonly SnykCodeClient snykCodeClient = new SnykCodeClient( + TestSettings.SnykCodeApiUrl, + TestSettings.Instance.ApiToken, + ContextFlowName, + ContextOrgName); [Fact] public async Task SnykCodeClient_TwoFilesWithIssuesProvided_GetAnalysisSuccessAsync() diff --git a/Snyk.Code.Library.Tests/SnykCode/TestSettings.cs b/Snyk.Code.Library.Tests/SnykCode/TestSettings.cs index acb836a9e..ea78c294f 100644 --- a/Snyk.Code.Library.Tests/SnykCode/TestSettings.cs +++ b/Snyk.Code.Library.Tests/SnykCode/TestSettings.cs @@ -1,9 +1,8 @@ -namespace Snyk.Code.Library.Tests.Api -{ - using System; - using Snyk.Common; - using Snyk.Common.Authentication; +using System; +using Snyk.Common.Authentication; +namespace Snyk.Code.Library.Tests.SnykCode +{ /// /// Test settings. /// diff --git a/Snyk.Common.Tests/Service/ApiEndpointResolverTest.cs b/Snyk.Common.Tests/Service/ApiEndpointResolverTest.cs index 7675238bd..7bf86b15a 100644 --- a/Snyk.Common.Tests/Service/ApiEndpointResolverTest.cs +++ b/Snyk.Common.Tests/Service/ApiEndpointResolverTest.cs @@ -1,11 +1,12 @@ +using System; +using Moq; +using Xunit; +using Snyk.Common.Authentication; +using Snyk.Common.Settings; +using Snyk.Common.Service; + namespace Snyk.Common.Tests.Service { - using Moq; - using Xunit; - using Snyk.Common.Authentication; - using Snyk.Common.Settings; - using Snyk.Common.Service; - /// /// Tests for . /// @@ -51,17 +52,40 @@ public void ApiEndpointResolver_GetSnykCodeApiUrl_SingleTenant() } [Fact] - public void ApiEndpointResolver_GetSnykCodeApiUrl_Snykgov() + public void ApiEndpointResolver_GetSnykCodeApiUrl_Snykgov_NoOrg() { var optionsMock = new Mock(); optionsMock .Setup(options => options.CustomEndpoint) .Returns("https://app.random-uuid.polaris.snykgov.io/api"); + optionsMock + .Setup(options => options.IsFedramp()) + .Returns(true); + + var apiEndpointResolver = new ApiEndpointResolver(optionsMock.Object); + + Assert.Throws(() => apiEndpointResolver.GetSnykCodeApiUrl()); + } + + [Fact] + public void ApiEndpointResolver_GetSnykCodeApiUrl_Snykgov() + { + var optionsMock = new Mock(); + optionsMock + .Setup(options => options.CustomEndpoint) + .Returns("https://app.random-uuid.polaris.snykgov.io/api"); + optionsMock + .Setup(options => options.IsFedramp()) + .Returns(true); + optionsMock + .Setup(options => options.Organization) + .Returns("dummy-org-name"); + var apiEndpointResolver = new ApiEndpointResolver(optionsMock.Object); var snykCodeApiUrl = apiEndpointResolver.GetSnykCodeApiUrl(); - Assert.Equal("https://deeproxy.random-uuid.polaris.snykgov.io/", snykCodeApiUrl); + Assert.Equal("https://api.random-uuid.polaris.snykgov.io/hidden/orgs/dummy-org-name/code/", snykCodeApiUrl); } [Fact] @@ -77,6 +101,9 @@ public void AuthenticationMethod() optionsMock .Setup(options => options.CustomEndpoint) .Returns("https://app.snykgov.io/api"); + optionsMock + .Setup(option => option.AuthenticationMethod) + .Returns(AuthenticationType.OAuth); // Assert Assert.Equal(AuthenticationType.OAuth, apiEndpointResolver.AuthenticationMethod); diff --git a/Snyk.Common.Tests/Service/SnykApiServiceTest.cs b/Snyk.Common.Tests/Service/SnykApiServiceTest.cs index d3ce9576c..005d1f31f 100644 --- a/Snyk.Common.Tests/Service/SnykApiServiceTest.cs +++ b/Snyk.Common.Tests/Service/SnykApiServiceTest.cs @@ -1,14 +1,14 @@ -namespace Snyk.VisualStudio.Extension.Tests +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using Snyk.Common.Authentication; +using Snyk.Common.Service; +using Snyk.Common.Settings; +using Xunit; + +namespace Snyk.Common.Tests.Service { - using System; - using System.Net.Http; - using System.Threading.Tasks; - using Moq; - using Snyk.Common.Authentication; - using Snyk.Common.Service; - using Snyk.Common.Settings; - using Xunit; - /// /// Tests for . /// diff --git a/Snyk.Common.Tests/Snyk.Common.Tests.csproj b/Snyk.Common.Tests/Snyk.Common.Tests.csproj index 568bf467b..8d0ccb66b 100644 --- a/Snyk.Common.Tests/Snyk.Common.Tests.csproj +++ b/Snyk.Common.Tests/Snyk.Common.Tests.csproj @@ -7,10 +7,13 @@ - + + + - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -20,6 +23,11 @@ + + true + true + + diff --git a/Snyk.Common/Authentication/AuthenticationToken.cs b/Snyk.Common/Authentication/AuthenticationToken.cs index 04ca99cdb..5db2031bf 100644 --- a/Snyk.Common/Authentication/AuthenticationToken.cs +++ b/Snyk.Common/Authentication/AuthenticationToken.cs @@ -24,7 +24,7 @@ public AuthenticationToken(AuthenticationType type, string value) public AuthenticationType Type { get; } - public override string ToString() + public string Refresh() { // if possible and required, update the token before using it if (this.TokenRefresher != null && Type == AuthenticationType.OAuth) @@ -43,6 +43,17 @@ public override string ToString() return this.value; } + public override string ToString() + { + return this.value; + } + + public bool IsValidAfterRefresh() + { + this.value = Refresh(); + return IsValid(); + } + public bool IsValid() { if (string.IsNullOrWhiteSpace(this.value)) @@ -57,12 +68,6 @@ public bool IsValid() case AuthenticationType.OAuth: { var tokenState = GetTokenState(this.value); - if (tokenState == OAuthTokenState.Expired) - { - this.value = this.TokenRefresher(); - tokenState = GetTokenState(this.value); - } - return tokenState == OAuthTokenState.Valid; } default: diff --git a/Snyk.Common/Authentication/AuthenticationType.cs b/Snyk.Common/Authentication/AuthenticationType.cs index 4a56d6305..83878089c 100644 --- a/Snyk.Common/Authentication/AuthenticationType.cs +++ b/Snyk.Common/Authentication/AuthenticationType.cs @@ -1,8 +1,12 @@ -namespace Snyk.Common.Authentication +using System.ComponentModel; + +namespace Snyk.Common.Authentication { public enum AuthenticationType { + [Description("Token authentication")] Token, + [Description("OAuth2 authentication")] OAuth, } } diff --git a/Snyk.Common/HttpClientFactory.cs b/Snyk.Common/HttpClientFactory.cs index 8aef46628..dfbdcaeff 100644 --- a/Snyk.Common/HttpClientFactory.cs +++ b/Snyk.Common/HttpClientFactory.cs @@ -37,7 +37,7 @@ public static HttpClient NewHttpClient(AuthenticationToken token, string baseUrl var authorizationString = "token " + token; if (token.Type == AuthenticationType.OAuth) { - var rawToken = token.ToString(); + var rawToken = token.Refresh(); var oauthToken = OAuthToken.FromJson(rawToken); var accessToken = oauthToken?.AccessToken; authorizationString = "bearer " + accessToken; diff --git a/Snyk.Common/Service/ApiEndpointResolver.cs b/Snyk.Common/Service/ApiEndpointResolver.cs index 08d95821e..318b90925 100644 --- a/Snyk.Common/Service/ApiEndpointResolver.cs +++ b/Snyk.Common/Service/ApiEndpointResolver.cs @@ -33,20 +33,7 @@ public ApiEndpointResolver(ISnykOptions options) /// public string UserMeEndpoint => SnykApiEndpoint + "/v1/user/me"; - public AuthenticationType AuthenticationMethod - { - get - { - var endpoint = ResolveCustomEndpoint(this.options.CustomEndpoint); - var endpointUri = new Uri(endpoint); - if (endpointUri.Host.Contains("snykgov.io")) - { - return AuthenticationType.OAuth; - } - - return AuthenticationType.Token; - } - } + public AuthenticationType AuthenticationMethod => this.options.AuthenticationMethod; /// /// Get SnykCode Settings url. @@ -73,8 +60,18 @@ public string GetSnykCodeApiUrl() var endpoint = ResolveCustomEndpoint(this.options.CustomEndpoint); - var result = GetCustomEndpointUrlFromSnykApi(endpoint, "deeproxy"); - + var isFedramp = this.options.IsFedramp(); + + if (isFedramp && string.IsNullOrEmpty(this.options.Organization)) + throw new InvalidOperationException("Organization is required in a fedramp environment"); + + var subDomain = isFedramp ? "api" : "deeproxy"; + + var result = GetCustomEndpointUrlFromSnykApi(endpoint, subDomain); + + if (isFedramp) + result += $"/hidden/orgs/{this.options.Organization}/code"; + return result + "/"; } diff --git a/Snyk.Common/Settings/ISnykOptions.cs b/Snyk.Common/Settings/ISnykOptions.cs index 468bd3fcc..6daed5b2d 100644 --- a/Snyk.Common/Settings/ISnykOptions.cs +++ b/Snyk.Common/Settings/ISnykOptions.cs @@ -1,27 +1,32 @@ -namespace Snyk.Common.Settings -{ - using System; - using System.Threading.Tasks; - using Snyk.Common.Authentication; - using Snyk.Common.Service; +using System; +using System.Threading.Tasks; +using Snyk.Common.Authentication; +using Snyk.Common.Service; +namespace Snyk.Common.Settings +{ /// /// Interface for Snyk Options/Settings in Visual Studio. /// public interface ISnykOptions { - String Application { get; set; } - String ApplicationVersion { get; set; } - String IntegrationName { get; } - String IntegrationVersion { get; } - String IntegrationEnvironment { get; set; } - String IntegrationEnvironmentVersion { get; set; } + string Application { get; set; } + string ApplicationVersion { get; set; } + string IntegrationName { get; } + string IntegrationVersion { get; } + string IntegrationEnvironment { get; set; } + string IntegrationEnvironmentVersion { get; set; } /// /// Gets or sets a value indicating whether Snyk user API token. /// AuthenticationToken ApiToken { get; } + /// + /// Gets Value of Authentication Token Type. + /// + AuthenticationType AuthenticationMethod { get; } + SnykUser SnykUser { get; set; } bool IsFedramp(); diff --git a/Snyk.VisualStudio.Extension.2022/CLI/SnykCli.cs b/Snyk.VisualStudio.Extension.2022/CLI/SnykCli.cs index 01b1f9db6..62ed13fa9 100644 --- a/Snyk.VisualStudio.Extension.2022/CLI/SnykCli.cs +++ b/Snyk.VisualStudio.Extension.2022/CLI/SnykCli.cs @@ -135,6 +135,10 @@ public void Authenticate() args.Add("--auth-type=oauth"); environmentVariables.Add("INTERNAL_SNYK_OAUTH_ENABLED", "1"); } + else + { + environmentVariables.Add("INTERNAL_SNYK_OAUTH_ENABLED", "0"); + } environmentVariables.Add(ApiEnvironmentVariableName, apiEndpointResolver.SnykApiEndpoint); @@ -147,7 +151,7 @@ public void Authenticate() } else { - var message = $"The `snyk auth` command failed to authenticate"; + var message = "The `snyk auth` command failed to authenticate"; throw new AuthenticationException(message); } } @@ -177,18 +181,17 @@ public string RunCommand(string arguments) var cliPath = this.GetCliPath(); Logger.Information("CLI path is {CliPath}", cliPath); - - var environmentVariables = new StringDictionary(); - var apiEndpointResolver = new ApiEndpointResolver(this.options); - environmentVariables.Add(ApiEnvironmentVariableName, apiEndpointResolver.SnykApiEndpoint); - + var environmentVariables = this.BuildScanEnvironmentVariables(false); this.ConsoleRunner.CreateProcess(cliPath, arguments, environmentVariables); Logger.Information("Start run console process"); var consoleResult = this.ConsoleRunner.Execute(); - Logger.Information("Start convert console string result to CliResult and return value"); + if (consoleResult.ToLower().Contains("error")) + { + throw new AuthenticationException(consoleResult); + } return consoleResult; } @@ -219,23 +222,16 @@ private string GetCliPath() return cliPath; } - public StringDictionary BuildScanEnvironmentVariables() + public StringDictionary BuildScanEnvironmentVariables(bool shouldRefreshToken = true) { var environmentVariables = new StringDictionary(); - if (this.Options.ApiToken.IsValid()) + if (this.Options.ApiToken.IsValid() || shouldRefreshToken) { - var token = this.Options.ApiToken.ToString(); - var tokenEnvVar = "SNYK_TOKEN"; - - if (this.Options.ApiToken.Type == AuthenticationType.OAuth) - { - tokenEnvVar = "INTERNAL_OAUTH_TOKEN_STORAGE"; - environmentVariables.Add("INTERNAL_SNYK_OAUTH_ENABLED", "1"); - } - - environmentVariables.Add(tokenEnvVar, token); - Logger.Information("Token added from Options"); + AddTokenToEnvVar(shouldRefreshToken, environmentVariables); } + environmentVariables.Add("INTERNAL_SNYK_OAUTH_ENABLED", this.Options.ApiToken.Type == AuthenticationType.OAuth ? "1" : "0"); + + Logger.Information("Token added from Options"); var apiEndpointResolver = new ApiEndpointResolver(this.options); environmentVariables.Add(ApiEnvironmentVariableName, apiEndpointResolver.SnykApiEndpoint); @@ -243,6 +239,17 @@ public StringDictionary BuildScanEnvironmentVariables() return environmentVariables; } + private void AddTokenToEnvVar(bool shouldRefreshToken, StringDictionary environmentVariables) + { + var token = shouldRefreshToken ? this.Options.ApiToken.Refresh() : this.Options.ApiToken.ToString(); + if (string.IsNullOrEmpty(token)) return; + + var internalTokenKey = this.Options.ApiToken.Type == AuthenticationType.OAuth + ? "INTERNAL_OAUTH_TOKEN_STORAGE" + : "SNYK_TOKEN"; + environmentVariables.Add(internalTokenKey, token); + } + /// /// Build arguments (options) for snyk cli depending on user settings. /// diff --git a/Snyk.VisualStudio.Extension.2022/Commands/AbstractTaskCommand.cs b/Snyk.VisualStudio.Extension.2022/Commands/AbstractTaskCommand.cs index b64035aba..e416a810a 100644 --- a/Snyk.VisualStudio.Extension.2022/Commands/AbstractTaskCommand.cs +++ b/Snyk.VisualStudio.Extension.2022/Commands/AbstractTaskCommand.cs @@ -27,7 +27,7 @@ public AbstractTaskCommand(AsyncPackage package, OleMenuCommandService commandSe protected bool IsButtonAvailable() { ThreadHelper.ThrowIfNotOnUIThread(); - return SnykVSPackage.ServiceProvider.Options.ApiToken.IsValid() + return SnykVSPackage.ServiceProvider.Options.ApiToken.IsValidAfterRefresh() && SnykSolutionService.Instance.IsSolutionOpen(); } diff --git a/Snyk.VisualStudio.Extension.2022/Commands/SnykCleanPanelCommand.cs b/Snyk.VisualStudio.Extension.2022/Commands/SnykCleanPanelCommand.cs index 7b9f7ba37..730aa5164 100644 --- a/Snyk.VisualStudio.Extension.2022/Commands/SnykCleanPanelCommand.cs +++ b/Snyk.VisualStudio.Extension.2022/Commands/SnykCleanPanelCommand.cs @@ -44,7 +44,7 @@ public static async Task InitializeAsync(AsyncPackage package) public override async Task UpdateStateAsync() { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - this.MenuCommand.Enabled = SnykVSPackage.ServiceProvider.Options.ApiToken.IsValid() + this.MenuCommand.Enabled = SnykVSPackage.ServiceProvider.Options.ApiToken.IsValidAfterRefresh() && this.VsPackage.ToolWindowControl.IsTreeContentNotEmpty(); } diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralOptionsDialogPage.cs b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralOptionsDialogPage.cs index e204d28ed..056577ca3 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralOptionsDialogPage.cs +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralOptionsDialogPage.cs @@ -56,13 +56,45 @@ public class SnykGeneralOptionsDialogPage : DialogPage, ISnykOptions /// Gets or sets a value indicating whether API token. /// public AuthenticationToken ApiToken => this.apiToken ?? AuthenticationToken.EmptyToken; + + public AuthenticationType AuthenticationMethod + { + get => this.userStorageSettingsService.AuthenticationMethod; + set + { + if (this.userStorageSettingsService == null || this.userStorageSettingsService.AuthenticationMethod == value) + return; + this.userStorageSettingsService.AuthenticationMethod = value; + // When changing the Token Type, the token is invalidated + InvalidateCurrentToken(); + this.FireSettingsChangedEvent(); + } + } private SastSettings sastSettings; private string RefreshToken() { + Logger.Information("Attempting to refresh OAuth token"); var cli = this.ServiceProvider?.NewCli(); - cli.RunCommand("whoami --experimental"); + if (cli == null) + { + Logger.Information("Couldn't get CLI. Aborting"); + return string.Empty; + } + + try + { + cli.RunCommand("whoami --experimental"); + } + catch (AuthenticationException ex) + { + Logger.Error("Failed to refresh access token: {Message}", ex.Message); + InvalidateCurrentToken(); + NotificationService.Instance?.ShowErrorInfoBar("Failed to refresh Access token"); + return string.Empty; + } + var token = cli.GetApiToken(); return token; } @@ -88,9 +120,12 @@ private AuthenticationToken CreateAuthenticationToken(string token) { var apiEndpointResolver = new ApiEndpointResolver(this); var type = apiEndpointResolver.AuthenticationMethod; + + var tokenObj = new AuthenticationToken(type, token) + { + TokenRefresher = RefreshToken + }; - var tokenObj = new AuthenticationToken(type, token); - tokenObj.TokenRefresher = RefreshToken; return tokenObj; } @@ -141,17 +176,21 @@ public string CustomEndpoint { return; } - // When changing the API endpoint, the API token is invalidated - this.apiToken = AuthenticationToken.EmptyToken; - var cli = this.ServiceProvider?.NewCli(); - cli?.UnsetApiToken(); // This setter can be called before initialization, so ServiceProvider can be null + InvalidateCurrentToken(); this.customEndpoint = newApiEndpoint; this.FireSettingsChangedEvent(); } } + private void InvalidateCurrentToken() + { + this.apiToken = AuthenticationToken.EmptyToken; + var cli = this.ServiceProvider?.NewCli(); + cli?.UnsetApiToken(); + } + /// public string SnykCodeSettingsUrl => $"{this.GetBaseAppUrl()}/manage/snyk-code"; @@ -366,6 +405,11 @@ public bool Authenticate() // Pull token from configuration. If the token is invalid, attempt to authenticate and get a new token. var apiTokenString = cli.GetApiToken(); var token = CreateAuthenticationToken(apiTokenString); + if (!token.IsValid() && token.Type == AuthenticationType.OAuth) + { + // Before re-authenticating attempt to refresh current token + token = CreateAuthenticationToken(token.Refresh()); + } if (!token.IsValid()) { Logger.Information("Api token is invalid. Attempting to authenticate via snyk auth"); diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.Designer.cs b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.Designer.cs index efeeecfaa..418623493 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.Designer.cs +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.Designer.cs @@ -49,6 +49,8 @@ private void InitializeComponent() this.codeSecurityEnabledCheckBox = new System.Windows.Forms.CheckBox(); this.codeQualityEnabledCheckBox = new System.Windows.Forms.CheckBox(); this.generalSettingsGroupBox = new System.Windows.Forms.GroupBox(); + this.authType = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); this.OrganizationInfoLink = new System.Windows.Forms.LinkLabel(); this.OrgDescriptionText = new System.Windows.Forms.Label(); this.richTextBox1 = new System.Windows.Forms.RichTextBox(); @@ -71,6 +73,7 @@ private void InitializeComponent() this.snykCodeQualityInfoToolTip = new System.Windows.Forms.ToolTip(this.components); this.customCliPathFileDialog = new System.Windows.Forms.OpenFileDialog(); this.ExecutablesGroupBox = new System.Windows.Forms.GroupBox(); + this.authMethodDescription = new System.Windows.Forms.RichTextBox(); ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).BeginInit(); this.generalSettingsGroupBox.SuspendLayout(); this.productSelectionGroupBox.SuspendLayout(); @@ -80,7 +83,7 @@ private void InitializeComponent() // // customEndpointTextBox // - this.customEndpointTextBox.Location = new System.Drawing.Point(100, 86); + this.customEndpointTextBox.Location = new System.Drawing.Point(129, 168); this.customEndpointTextBox.Margin = new System.Windows.Forms.Padding(2); this.customEndpointTextBox.Name = "customEndpointTextBox"; this.customEndpointTextBox.Size = new System.Drawing.Size(300, 20); @@ -91,7 +94,7 @@ private void InitializeComponent() // customEndpointLabel // this.customEndpointLabel.AutoSize = true; - this.customEndpointLabel.Location = new System.Drawing.Point(4, 89); + this.customEndpointLabel.Location = new System.Drawing.Point(4, 171); this.customEndpointLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.customEndpointLabel.Name = "customEndpointLabel"; this.customEndpointLabel.Size = new System.Drawing.Size(89, 13); @@ -101,7 +104,7 @@ private void InitializeComponent() // organizationLabel // this.organizationLabel.AutoSize = true; - this.organizationLabel.Location = new System.Drawing.Point(4, 135); + this.organizationLabel.Location = new System.Drawing.Point(4, 217); this.organizationLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.organizationLabel.Name = "organizationLabel"; this.organizationLabel.Size = new System.Drawing.Size(69, 13); @@ -110,7 +113,7 @@ private void InitializeComponent() // // organizationTextBox // - this.organizationTextBox.Location = new System.Drawing.Point(100, 134); + this.organizationTextBox.Location = new System.Drawing.Point(129, 216); this.organizationTextBox.Margin = new System.Windows.Forms.Padding(2); this.organizationTextBox.Name = "organizationTextBox"; this.organizationTextBox.Size = new System.Drawing.Size(300, 20); @@ -120,7 +123,7 @@ private void InitializeComponent() // tokenLabel // this.tokenLabel.AutoSize = true; - this.tokenLabel.Location = new System.Drawing.Point(4, 57); + this.tokenLabel.Location = new System.Drawing.Point(4, 139); this.tokenLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.tokenLabel.Name = "tokenLabel"; this.tokenLabel.Size = new System.Drawing.Size(41, 13); @@ -129,7 +132,7 @@ private void InitializeComponent() // // tokenTextBox // - this.tokenTextBox.Location = new System.Drawing.Point(100, 54); + this.tokenTextBox.Location = new System.Drawing.Point(129, 136); this.tokenTextBox.Name = "tokenTextBox"; this.tokenTextBox.PasswordChar = '*'; this.tokenTextBox.Size = new System.Drawing.Size(300, 20); @@ -140,7 +143,7 @@ private void InitializeComponent() // ignoreUnknownCACheckBox // this.ignoreUnknownCACheckBox.AutoSize = true; - this.ignoreUnknownCACheckBox.Location = new System.Drawing.Point(100, 107); + this.ignoreUnknownCACheckBox.Location = new System.Drawing.Point(129, 189); this.ignoreUnknownCACheckBox.Margin = new System.Windows.Forms.Padding(2); this.ignoreUnknownCACheckBox.Name = "ignoreUnknownCACheckBox"; this.ignoreUnknownCACheckBox.Size = new System.Drawing.Size(120, 17); @@ -151,9 +154,9 @@ private void InitializeComponent() // // authenticateButton // - this.authenticateButton.Location = new System.Drawing.Point(100, 20); + this.authenticateButton.Location = new System.Drawing.Point(127, 104); this.authenticateButton.Name = "authenticateButton"; - this.authenticateButton.Size = new System.Drawing.Size(193, 20); + this.authenticateButton.Size = new System.Drawing.Size(193, 26); this.authenticateButton.TabIndex = 7; this.authenticateButton.Text = "Connect Visual Studio to Snyk.io"; this.authenticateButton.UseVisualStyleBackColor = true; @@ -161,7 +164,7 @@ private void InitializeComponent() // // authProgressBar // - this.authProgressBar.Location = new System.Drawing.Point(100, 76); + this.authProgressBar.Location = new System.Drawing.Point(129, 158); this.authProgressBar.Margin = new System.Windows.Forms.Padding(2, 3, 2, 3); this.authProgressBar.MarqueeAnimationSpeed = 10; this.authProgressBar.Name = "authProgressBar"; @@ -182,7 +185,7 @@ private void InitializeComponent() this.usageAnalyticsCheckBox.Location = new System.Drawing.Point(12, 30); this.usageAnalyticsCheckBox.Margin = new System.Windows.Forms.Padding(2); this.usageAnalyticsCheckBox.Name = "usageAnalyticsCheckBox"; - this.usageAnalyticsCheckBox.Size = new System.Drawing.Size(127, 17); + this.usageAnalyticsCheckBox.Size = new System.Drawing.Size(165, 17); this.usageAnalyticsCheckBox.TabIndex = 9; this.usageAnalyticsCheckBox.Text = "Send usage statistics to Snyk"; this.usageAnalyticsCheckBox.UseVisualStyleBackColor = true; @@ -232,6 +235,9 @@ private void InitializeComponent() // // generalSettingsGroupBox // + this.generalSettingsGroupBox.Controls.Add(this.authMethodDescription); + this.generalSettingsGroupBox.Controls.Add(this.authType); + this.generalSettingsGroupBox.Controls.Add(this.label2); this.generalSettingsGroupBox.Controls.Add(this.OrganizationInfoLink); this.generalSettingsGroupBox.Controls.Add(this.OrgDescriptionText); this.generalSettingsGroupBox.Controls.Add(this.tokenLabel); @@ -247,15 +253,38 @@ private void InitializeComponent() this.generalSettingsGroupBox.Margin = new System.Windows.Forms.Padding(8); this.generalSettingsGroupBox.Name = "generalSettingsGroupBox"; this.generalSettingsGroupBox.Padding = new System.Windows.Forms.Padding(2); - this.generalSettingsGroupBox.Size = new System.Drawing.Size(560, 233); + this.generalSettingsGroupBox.Size = new System.Drawing.Size(560, 327); this.generalSettingsGroupBox.TabIndex = 17; this.generalSettingsGroupBox.TabStop = false; this.generalSettingsGroupBox.Text = "General Settings"; // + // authType + // + this.authType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.authType.FormattingEnabled = true; + this.authType.Items.AddRange(new object[] { + "OAuth", + "Token"}); + this.authType.Location = new System.Drawing.Point(129, 26); + this.authType.Name = "authType"; + this.authType.Size = new System.Drawing.Size(193, 21); + this.authType.TabIndex = 13; + this.authType.SelectionChangeCommitted += new System.EventHandler(this.authType_SelectionChangeCommitted); + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(4, 29); + this.label2.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(120, 13); + this.label2.TabIndex = 12; + this.label2.Text = " Authentication Method:"; + // // OrganizationInfoLink // this.OrganizationInfoLink.AutoSize = true; - this.OrganizationInfoLink.Location = new System.Drawing.Point(108, 205); + this.OrganizationInfoLink.Location = new System.Drawing.Point(137, 287); this.OrganizationInfoLink.Name = "OrganizationInfoLink"; this.OrganizationInfoLink.Size = new System.Drawing.Size(150, 13); this.OrganizationInfoLink.TabIndex = 11; @@ -266,7 +295,7 @@ private void InitializeComponent() // OrgDescriptionText // this.OrgDescriptionText.AutoSize = true; - this.OrgDescriptionText.Location = new System.Drawing.Point(108, 156); + this.OrgDescriptionText.Location = new System.Drawing.Point(137, 238); this.OrgDescriptionText.Name = "OrgDescriptionText"; this.OrgDescriptionText.Size = new System.Drawing.Size(376, 39); this.OrgDescriptionText.TabIndex = 10; @@ -287,7 +316,7 @@ private void InitializeComponent() // // resetCliPathToDefaultButton // - this.resetCliPathToDefaultButton.Location = new System.Drawing.Point(179, 26); + this.resetCliPathToDefaultButton.Location = new System.Drawing.Point(208, 26); this.resetCliPathToDefaultButton.Name = "resetCliPathToDefaultButton"; this.resetCliPathToDefaultButton.Size = new System.Drawing.Size(97, 23); this.resetCliPathToDefaultButton.TabIndex = 17; @@ -297,7 +326,7 @@ private void InitializeComponent() // // CliPathBrowseButton // - this.CliPathBrowseButton.Location = new System.Drawing.Point(98, 26); + this.CliPathBrowseButton.Location = new System.Drawing.Point(127, 26); this.CliPathBrowseButton.Name = "CliPathBrowseButton"; this.CliPathBrowseButton.Size = new System.Drawing.Size(75, 23); this.CliPathBrowseButton.TabIndex = 16; @@ -307,7 +336,7 @@ private void InitializeComponent() // // CliPathTextBox // - this.CliPathTextBox.Location = new System.Drawing.Point(100, 51); + this.CliPathTextBox.Location = new System.Drawing.Point(129, 51); this.CliPathTextBox.Margin = new System.Windows.Forms.Padding(2); this.CliPathTextBox.Name = "CliPathTextBox"; this.CliPathTextBox.ReadOnly = true; @@ -356,7 +385,7 @@ private void InitializeComponent() this.productSelectionGroupBox.Controls.Add(this.codeQualityEnabledCheckBox); this.productSelectionGroupBox.Controls.Add(this.ossEnabledCheckBox); this.productSelectionGroupBox.Controls.Add(this.codeSecurityEnabledCheckBox); - this.productSelectionGroupBox.Location = new System.Drawing.Point(10, 409); + this.productSelectionGroupBox.Location = new System.Drawing.Point(10, 505); this.productSelectionGroupBox.Margin = new System.Windows.Forms.Padding(2); this.productSelectionGroupBox.Name = "productSelectionGroupBox"; this.productSelectionGroupBox.Padding = new System.Windows.Forms.Padding(8); @@ -443,7 +472,7 @@ private void InitializeComponent() // userExperienceGroupBox // this.userExperienceGroupBox.Controls.Add(this.usageAnalyticsCheckBox); - this.userExperienceGroupBox.Location = new System.Drawing.Point(10, 547); + this.userExperienceGroupBox.Location = new System.Drawing.Point(10, 643); this.userExperienceGroupBox.Margin = new System.Windows.Forms.Padding(2); this.userExperienceGroupBox.Name = "userExperienceGroupBox"; this.userExperienceGroupBox.Padding = new System.Windows.Forms.Padding(8); @@ -481,13 +510,24 @@ private void InitializeComponent() this.ExecutablesGroupBox.Controls.Add(this.CliPathBrowseButton); this.ExecutablesGroupBox.Controls.Add(this.ManageBinariesAutomaticallyCheckbox); this.ExecutablesGroupBox.Controls.Add(this.CliPathTextBox); - this.ExecutablesGroupBox.Location = new System.Drawing.Point(10, 254); + this.ExecutablesGroupBox.Location = new System.Drawing.Point(10, 348); this.ExecutablesGroupBox.Name = "ExecutablesGroupBox"; this.ExecutablesGroupBox.Size = new System.Drawing.Size(560, 152); this.ExecutablesGroupBox.TabIndex = 19; this.ExecutablesGroupBox.TabStop = false; this.ExecutablesGroupBox.Text = "Executables Settings"; // + // authMethodDescription + // + this.authMethodDescription.BackColor = System.Drawing.SystemColors.Control; + this.authMethodDescription.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.authMethodDescription.Location = new System.Drawing.Point(127, 53); + this.authMethodDescription.Name = "authMethodDescription"; + this.authMethodDescription.ReadOnly = true; + this.authMethodDescription.Size = new System.Drawing.Size(428, 45); + this.authMethodDescription.TabIndex = 19; + this.authMethodDescription.Text = resources.GetString("authMethodDescription.Text"); + // // SnykGeneralSettingsUserControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -553,5 +593,8 @@ private void InitializeComponent() private Button resetCliPathToDefaultButton; private RichTextBox richTextBox1; private GroupBox ExecutablesGroupBox; + private Label label2; + private ComboBox authType; + private RichTextBox authMethodDescription; } } diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.cs b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.cs index 26fc9be98..1bf7da23e 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.cs +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.cs @@ -1,21 +1,25 @@ -namespace Snyk.VisualStudio.Extension.Settings +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using Serilog; +using Snyk.Common; +using Snyk.Common.Authentication; +using Snyk.Common.Service; +using Snyk.Common.Settings; +using Snyk.VisualStudio.Extension.CLI; +using Snyk.VisualStudio.Extension.Service; +using Snyk.VisualStudio.Extension.UI.Notifications; +using Task = System.Threading.Tasks.Task; + +namespace Snyk.VisualStudio.Extension.Settings { - using System; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Threading.Tasks; - using System.Windows.Forms; - using Microsoft.VisualStudio.Shell; - using Microsoft.VisualStudio.Threading; - using Serilog; - using Snyk.Common; - using Snyk.Common.Service; - using Snyk.Common.Settings; - using Snyk.VisualStudio.Extension.CLI; - using Snyk.VisualStudio.Extension.Service; - using Snyk.VisualStudio.Extension.UI.Notifications; - using Task = System.Threading.Tasks.Task; - /// /// Control for Snyk General Settings. /// @@ -79,6 +83,26 @@ private void UpdateViewFromOptionsDialog() : this.OptionsDialogPage.CliCustomPath; this.CliPathTextBox.Text = cliPath; + + if (authType.SelectedIndex == -1) + { + this.authType.DataSource = AuthenticationMethodList(); + this.authType.DisplayMember = "Description"; + this.authType.ValueMember = "Value"; + } + this.authType.SelectedValue = this.OptionsDialogPage.AuthenticationMethod; + } + + private static IEnumerable AuthenticationMethodList() + { + return Enum.GetValues(typeof(AuthenticationType)) + .Cast() + .Select(value => new + { + (Attribute.GetCustomAttribute(value.GetType().GetField(value.ToString()), typeof(DescriptionAttribute)) as DescriptionAttribute)?.Description, + Value = value + }) + .ToList(); } private void OptionsDialogPageOnSettingsChanged(object sender, SnykSettingsChangedEventArgs e) => @@ -143,7 +167,7 @@ private void InitializeApiToken() } } - this.tokenTextBox.Text = this.OptionsDialogPage.ApiToken.ToString(); + this.tokenTextBox.Text = this.OptionsDialogPage.ApiToken.Refresh(); } private async Task OnAuthenticationFailAsync(string errorMessage) @@ -462,5 +486,10 @@ private void ClearCliCustomPathButton_Click(object sender, EventArgs e) { this.SetCliCustomPathValue(string.Empty); } + + private void authType_SelectionChangeCommitted(object sender, EventArgs e) + { + this.OptionsDialogPage.AuthenticationMethod = (AuthenticationType)authType.SelectedValue; + } } } diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.resx b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.resx index df44b88d8..0f6304ba9 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.resx +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykGeneralSettingsUserControl.resx @@ -123,15 +123,6 @@ True - - True - - - True - - - True - @@ -148,9 +139,6 @@ True - - 463, 17 - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO @@ -160,12 +148,12 @@ jZZtldeAJuTjx7SYAQYQKn7xnCU8ABJO2R9gGis+AAAAAElFTkSuQmCC + + 463, 17 + True - - 232, 17 - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO @@ -178,9 +166,10 @@ 232, 17 - - 463, 17 - + + Specifies whether to authenticate with OAuth2 or with an API token. +Note: OAuth2 is currently experimental. Once it is fully supported, using OAuth2 authentication is recommended as it provides enhanced security. + 1056, 17 diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykSettings.cs b/Snyk.VisualStudio.Extension.2022/Settings/SnykSettings.cs index 5230b40f4..30e866a04 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykSettings.cs +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykSettings.cs @@ -1,4 +1,6 @@ -namespace Snyk.VisualStudio.Extension.Settings +using Snyk.Common.Authentication; + +namespace Snyk.VisualStudio.Extension.Settings { using System; using System.Collections.Generic; @@ -69,5 +71,10 @@ public SnykSettings() /// Gets or sets an array of workspace trusted folders. /// public ISet TrustedFolders { get; set; } = new HashSet(); + + /// + /// Gets or sets Authentication Type. + /// + public AuthenticationType AuthenticationMethod { get; set; } } } \ No newline at end of file diff --git a/Snyk.VisualStudio.Extension.2022/Settings/SnykUserStorageSettingsService.cs b/Snyk.VisualStudio.Extension.2022/Settings/SnykUserStorageSettingsService.cs index 27c5c7c9a..13d238d88 100644 --- a/Snyk.VisualStudio.Extension.2022/Settings/SnykUserStorageSettingsService.cs +++ b/Snyk.VisualStudio.Extension.2022/Settings/SnykUserStorageSettingsService.cs @@ -1,12 +1,13 @@ -namespace Snyk.VisualStudio.Extension.Settings +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Serilog; +using Snyk.Common; +using Snyk.Common.Authentication; +using Snyk.VisualStudio.Extension.Service; + +namespace Snyk.VisualStudio.Extension.Settings { - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Serilog; - using Snyk.Common; - using Snyk.VisualStudio.Extension.Service; - /// /// Service for solution settings. /// @@ -52,6 +53,17 @@ public string CliCustomPath } } + public AuthenticationType AuthenticationMethod + { + get => this.LoadSettings().AuthenticationMethod; + set + { + var settings = this.LoadSettings(); + settings.AuthenticationMethod = value; + this.settingsLoader.Save(settings); + } + } + /// /// Gets or sets trusted folders list. /// diff --git a/Snyk.VisualStudio.Extension.2022/Snyk.VisualStudio.Extension.2022.csproj b/Snyk.VisualStudio.Extension.2022/Snyk.VisualStudio.Extension.2022.csproj index 1e1c445c2..1f076fd2d 100644 --- a/Snyk.VisualStudio.Extension.2022/Snyk.VisualStudio.Extension.2022.csproj +++ b/Snyk.VisualStudio.Extension.2022/Snyk.VisualStudio.Extension.2022.csproj @@ -547,6 +547,7 @@ SnykGeneralSettingsUserControl.cs + Designer SnykSolutionOptionsUserControl.cs diff --git a/Snyk.VisualStudio.Extension.2022/SnykVSPackage.cs b/Snyk.VisualStudio.Extension.2022/SnykVSPackage.cs index 70a1de593..42f38db84 100644 --- a/Snyk.VisualStudio.Extension.2022/SnykVSPackage.cs +++ b/Snyk.VisualStudio.Extension.2022/SnykVSPackage.cs @@ -2,7 +2,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; diff --git a/Snyk.VisualStudio.Extension.2022/UI/Notifications/VsInfoBarService.cs b/Snyk.VisualStudio.Extension.2022/UI/Notifications/VsInfoBarService.cs index f60fc94e1..7c27317fc 100644 --- a/Snyk.VisualStudio.Extension.2022/UI/Notifications/VsInfoBarService.cs +++ b/Snyk.VisualStudio.Extension.2022/UI/Notifications/VsInfoBarService.cs @@ -81,11 +81,9 @@ public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBar public void ShowErrorInfoBar(string message) => ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - if (this.messagesCache.ContainsKey(message)) - { + + if (this.serviceProvider.Package.ToolWindow == null || this.messagesCache.ContainsKey(message)) return; - } var text = new InfoBarTextSpan(message); var submitIssueLink = new InfoBarHyperlink("Contact support", ContactSupport); @@ -102,7 +100,7 @@ public void ShowErrorInfoBar(string message) => ThreadHelper.JoinableTaskFactory element.Advise(this, out this.cookie); this.messagesCache.Add(message, element); - + this.serviceProvider.Package.ToolWindow.AddInfoBar(element); }); } diff --git a/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs b/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs index d57a049fd..f6e8578cc 100644 --- a/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs +++ b/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs @@ -245,7 +245,7 @@ public void SnykCliTest_BuildEnvironmentVariables_InvalidToken() var result = cli.BuildScanEnvironmentVariables(); - Assert.Equal(1, result.Count); + Assert.Equal(2, result.Count); } [Fact] diff --git a/Snyk.VisualStudio.Extension.Tests/UI/ResourceLoaderTest.cs b/Snyk.VisualStudio.Extension.Tests/UI/ResourceLoaderTest.cs index 1a400ad70..fc3edf106 100644 --- a/Snyk.VisualStudio.Extension.Tests/UI/ResourceLoaderTest.cs +++ b/Snyk.VisualStudio.Extension.Tests/UI/ResourceLoaderTest.cs @@ -2,18 +2,19 @@ using System; using Snyk.VisualStudio.Extension.UI; using Xunit; +using Microsoft.VisualStudio.Sdk.TestFramework; namespace Snyk.VisualStudio.Extension.Tests.UI { /// /// Unit tests for . /// + [Collection(MockedVS.Collection)] public class ResourceLoaderTest { private const string AssemblyName = "Snyk.VisualStudio.Extension.Tests"; private const string ResourcesDirectory = "/Resources"; - - public ResourceLoaderTest() + public ResourceLoaderTest(GlobalServiceProvider sp) { var resourcesType = typeof(ResourceLoader); @@ -27,10 +28,8 @@ public ResourceLoaderTest() resourcesType.GetField("_resourcesDirectory", BindingFlags.NonPublic | BindingFlags.Static)? .SetValue(null, ResourcesDirectory); + sp.Reset(); - if (!UriParser.IsKnownScheme("pack")) - // ReSharper disable once ObjectCreationAsStatement - new System.Windows.Application(); } [Fact]