Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLVS-1498 Show infobar when server certificate can't be verified #5942

1 change: 1 addition & 0 deletions src/Core/DocumentationLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static class DocumentationLinks
public const string OpenInIdeIssueLocation = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/troubleshooting/#no-matching-issue-found";
public const string OpenInIdeBindingSetup = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/troubleshooting/#no-matching-project-found";
public const string UnbindingProject = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/connected-mode-setup/#unbinding-a-project";
public const string SslCertificate = "https://docs.sonarsource.com/sonarqube-for-ide/intellij/team-features/advanced-configuration/#client-ssl-certificates";
gabriela-trutan-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

public static readonly Uri UnbindingProjectUri = new(UnbindingProject);
public static readonly Uri ConnectedModeUri = new(ConnectedMode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Security.Cryptography.X509Certificates;
using NSubstitute.ExceptionExtensions;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Notifications;
using SonarLint.VisualStudio.SLCore.Core;
using SonarLint.VisualStudio.SLCore.Listener.Http;
using SonarLint.VisualStudio.SLCore.Listener.Http.Model;
Expand All @@ -37,6 +38,9 @@ public class HttpConfigurationListenerTests
private ISystemProxyDetector proxySettingsDetector;
private TestLogger testLogger;
private HttpConfigurationListener testSubject;
private INotificationService notificationService;
private IBrowserService browserService;
private IOutputWindowService outputWindowService;

[TestInitialize]
public void TestInitialize()
Expand All @@ -45,7 +49,17 @@ public void TestInitialize()
certificateChainValidator = Substitute.For<ICertificateChainValidator>();
certificateDtoConverter = Substitute.For<ICertificateDtoConverter>();
proxySettingsDetector = Substitute.For<ISystemProxyDetector>();
testSubject = new HttpConfigurationListener(testLogger, certificateChainValidator, certificateDtoConverter, proxySettingsDetector);
notificationService = Substitute.For<INotificationService>();
browserService = Substitute.For<IBrowserService>();
outputWindowService = Substitute.For<IOutputWindowService>();

testSubject = new HttpConfigurationListener(testLogger,
certificateChainValidator,
certificateDtoConverter,
proxySettingsDetector,
notificationService,
browserService,
outputWindowService);
}

[TestMethod]
Expand All @@ -54,7 +68,10 @@ public void MefCtor_CheckIsExported() =>
MefTestHelpers.CreateExport<ILogger>(),
MefTestHelpers.CreateExport<ICertificateDtoConverter>(),
MefTestHelpers.CreateExport<ICertificateChainValidator>(),
MefTestHelpers.CreateExport<ISystemProxyDetector>()
MefTestHelpers.CreateExport<ISystemProxyDetector>(),
MefTestHelpers.CreateExport<INotificationService>(),
MefTestHelpers.CreateExport<IBrowserService>(),
MefTestHelpers.CreateExport<IOutputWindowService>()
);

[TestMethod]
Expand Down Expand Up @@ -168,6 +185,28 @@ public async Task CheckServerTrustedAsync_Exception_ReturnsFalse()
testLogger.AssertPartialOutputStringExists(exceptionReason);
}

[TestMethod]
public async Task CheckServerTrustedAsync_CertificateIsNotValid_ShowsNotification()
{
var (primaryCertificateDto, primaryCertificate) = SetUpCertificate("some certificate");
certificateChainValidator.ValidateChain(primaryCertificate, Arg.Is<IEnumerable<X509Certificate2>>(x => !x.Any())).Returns(false);

await testSubject.CheckServerTrustedAsync(new CheckServerTrustedParams([primaryCertificateDto], "ignored"));

notificationService.Received(1).ShowNotification(Arg.Is<INotification>(x => IsExpectedNotification(x)));
}

[TestMethod]
public async Task CheckServerTrustedAsync_CertificateIsValid_DoesNotShowNotification()
{
var (primaryCertificateDto, primaryCertificate) = SetUpCertificate("some certificate");
certificateChainValidator.ValidateChain(primaryCertificate, Arg.Is<IEnumerable<X509Certificate2>>(x => !x.Any())).Returns(true);

await testSubject.CheckServerTrustedAsync(new CheckServerTrustedParams([primaryCertificateDto], "ignored"));

notificationService.DidNotReceive().ShowNotification(Arg.Any<INotification>());
}

private (X509CertificateDto certificateDto, X509Certificate2 certificate) SetUpCertificate(string certificateName)
{
var certificateDto = new X509CertificateDto(certificateName);
Expand All @@ -181,4 +220,25 @@ public async Task CheckServerTrustedAsync_Exception_ReturnsFalse()
private void MockProxyConfigured(string hostName, int port) => proxySettingsDetector.GetProxyUri(Arg.Any<Uri>()).Returns(new Uri($"{hostName}:{port}"));

private static string BuildUri(string scheme, string host) => $"{scheme}://{host}";

private bool IsExpectedNotification(INotification x)
{
VerifyNotificationHasExpectedActions(x);

return x.Id == HttpConfigurationListener.ServerCertificateInvalidNotificationId && x.Message == SLCoreStrings.ServerCertificateInfobar_CertificateInvalidMessage;
}

private void VerifyNotificationHasExpectedActions(INotification notification)
{
notification.Actions.Should().HaveCount(2);
notification.Actions.Should().Contain(x => IsExpectedAction(x, SLCoreStrings.ServerCertificateInfobar_LearnMore));
notification.Actions.Should().Contain(x => IsExpectedAction(x, SLCoreStrings.ServerCertificateInfobar_ShowLogs));

notification.Actions.First().Action.Invoke(null);
notification.Actions.Last().Action.Invoke(null);
browserService.Received(1).Navigate(DocumentationLinks.SslCertificate);
outputWindowService.Received(1).Show();
}

private static bool IsExpectedAction(INotificationAction notificationAction, string expectedText) => notificationAction.CommandText == expectedText && !notificationAction.ShouldDismissAfterAction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

using System.ComponentModel.Composition;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Notifications;
using SonarLint.VisualStudio.SLCore.Core;
using SonarLint.VisualStudio.SLCore.Listener.Http;
using SonarLint.VisualStudio.SLCore.Listener.Http.Model;
Expand All @@ -34,15 +35,28 @@ internal class HttpConfigurationListener : IHttpConfigurationListener
private readonly ICertificateChainValidator chainValidator;
private readonly ICertificateDtoConverter certificateDtoConverter;
private readonly ISystemProxyDetector proxySettingsDetector;
private readonly INotificationService notificationService;
private readonly IBrowserService browserService;
private readonly IOutputWindowService outputWindowService;
private static readonly List<string> socksHosts = ["socks4", "socks5"];
public const string ServerCertificateInvalidNotificationId = "ServerCertificateInvalidNotificationId";

[ImportingConstructor]
public HttpConfigurationListener(ILogger logger, ICertificateChainValidator chainValidator, ICertificateDtoConverter certificateDtoConverter, ISystemProxyDetector proxySettingsDetector)
public HttpConfigurationListener(ILogger logger,
ICertificateChainValidator chainValidator,
ICertificateDtoConverter certificateDtoConverter,
ISystemProxyDetector proxySettingsDetector,
INotificationService notificationService,
IBrowserService browserService,
IOutputWindowService outputWindowService)
{
this.logger = logger;
this.chainValidator = chainValidator;
this.certificateDtoConverter = certificateDtoConverter;
this.proxySettingsDetector = proxySettingsDetector;
this.notificationService = notificationService;
this.browserService = browserService;
this.outputWindowService = outputWindowService;
}

public Task<SelectProxiesResponse> SelectProxiesAsync(SelectProxiesParams parameters)
Expand Down Expand Up @@ -84,11 +98,23 @@ public Task<CheckServerTrustedResponse> CheckServerTrustedAsync(CheckServerTrust
{
logger.WriteLine(SLCoreStrings.HttpConfiguration_ServerTrustVerificationRequest);
var verificationResult = VerifyChain(parameters.chain);
if (!verificationResult)
{
notificationService.ShowNotification(GetServerCertificateInvalidNotification());
}
logger.WriteLine(SLCoreStrings.HttpConfiguration_ServerTrustVerificationResult, verificationResult);
gabriela-trutan-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

return Task.FromResult(new CheckServerTrustedResponse(verificationResult));
}

private VisualStudio.Core.Notifications.Notification GetServerCertificateInvalidNotification() =>
gabriela-trutan-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
new(ServerCertificateInvalidNotificationId,
gabriela-trutan-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
SLCoreStrings.ServerCertificateInfobar_CertificateInvalidMessage,
[
new NotificationAction(SLCoreStrings.ServerCertificateInfobar_LearnMore, _ => browserService.Navigate(DocumentationLinks.SslCertificate), false),
new NotificationAction(SLCoreStrings.ServerCertificateInfobar_ShowLogs, _ => outputWindowService.Show(), false)
]);

private bool VerifyChain(List<X509CertificateDto> chain)
{
try
Expand Down
27 changes: 27 additions & 0 deletions src/SLCore/SLCoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/SLCore/SLCoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,13 @@
<data name="SLCoreName" xml:space="preserve">
<value>SLCore</value>
</data>
<data name="ServerCertificateInfobar_CertificateInvalidMessage" xml:space="preserve">
<value>The server certificate can not be verified. Please see the logs for more info.</value>
</data>
<data name="ServerCertificateInfobar_LearnMore" xml:space="preserve">
<value>Learn more</value>
</data>
<data name="ServerCertificateInfobar_ShowLogs" xml:space="preserve">
<value>Show logs</value>
</data>
</root>
Loading