diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/RequestHandlerBindingAssistant.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/RequestHandlerBindingAssistant.java index 94f9f076b3..acccc43722 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/RequestHandlerBindingAssistant.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/RequestHandlerBindingAssistant.java @@ -20,10 +20,14 @@ package org.sonarsource.sonarlint.core.embedded.server; import com.google.common.util.concurrent.MoreExecutors; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.PreDestroy; @@ -32,6 +36,7 @@ import org.sonarsource.sonarlint.core.BindingCandidatesFinder; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; +import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; @@ -78,13 +83,13 @@ public RequestHandlerBindingAssistant(BindingSuggestionProvider bindingSuggestio } interface Callback { - void andThen(String connectionId, @Nullable String configurationScopeId, SonarLintCancelMonitor cancelMonitor); + void andThen(String connectionId, Collection boundScopes, @Nullable String configurationScopeId, SonarLintCancelMonitor cancelMonitor); } - void assistConnectionAndBindingIfNeededAsync(AssistCreatingConnectionParams connectionParams, String projectKey, Callback callback) { + void assistConnectionAndBindingIfNeededAsync(AssistCreatingConnectionParams connectionParams, String projectKey, String origin, Callback callback) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); - executorService.submit(() -> assistConnectionAndBindingIfNeeded(connectionParams, projectKey, callback, cancelMonitor)); + executorService.submit(() -> assistConnectionAndBindingIfNeeded(connectionParams, projectKey, origin, callback, cancelMonitor)); } private void assistConnectionAndBindingIfNeeded(AssistCreatingConnectionParams connectionParams, String projectKey, String origin, @@ -102,19 +107,22 @@ private void assistConnectionAndBindingIfNeeded(AssistCreatingConnectionParams c var assistNewConnectionResult = assistCreatingConnectionAndWaitForRepositoryUpdate(connectionParams, cancelMonitor); var assistNewBindingResult = assistBindingAndWaitForRepositoryUpdate(assistNewConnectionResult.getNewConnectionId(), isSonarCloud, projectKey, cancelMonitor); - callback.andThen(assistNewConnectionResult.getNewConnectionId(), assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); + var boundScopes = new HashSet(); + if (assistNewBindingResult.getConfigurationScopeId() != null) { + boundScopes.add(assistNewBindingResult.getConfigurationScopeId()); + } + callback.andThen(assistNewConnectionResult.getNewConnectionId(), boundScopes, assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); } finally { endFullBindingProcess(); } } else { - // Should we check that the origin is matching the serverUrl URI? var isOriginTrusted = repository.hasConnectionWithOrigin(origin); if (isOriginTrusted) { // we pick the first connection but this could lead to issues later if there were several matches (make the user select the right // one?) assistBindingIfNeeded(connectionsMatchingOrigin.get(0).getConnectionId(), isSonarCloud, projectKey, callback, cancelMonitor); } else { - LOG.warn("The origin " + origin + " is not trusted, this could be a malicious request"); + LOG.warn("The origin '" + origin + "' is not trusted, this could be a malicious request"); client.showMessage(new ShowMessageParams(MessageType.ERROR, "SonarQube for IDE received a non-trusted request and could not proceed with it. " + "See logs for more details.")); } @@ -153,10 +161,15 @@ private void assistBindingIfNeeded(String connectionId, boolean isSonarCloud, St var scopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); if (scopes.isEmpty()) { var assistNewBindingResult = assistBindingAndWaitForRepositoryUpdate(connectionId, isSonarCloud, projectKey, cancelMonitor); - callback.andThen(connectionId, assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); + var boundScopes = new HashSet(); + if (assistNewBindingResult.getConfigurationScopeId() != null) { + boundScopes.add(assistNewBindingResult.getConfigurationScopeId()); + } + callback.andThen(connectionId, boundScopes, assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); } else { + var boundScopes = scopes.stream().map(BoundScope::getConfigScopeId).filter(Objects::nonNull).collect(Collectors.toSet()); // we pick the first bound scope but this could lead to issues later if there were several matches (make the user select the right one?) - callback.andThen(connectionId, scopes.iterator().next().getConfigScopeId(), cancelMonitor); + callback.andThen(connectionId, boundScopes, scopes.iterator().next().getConfigScopeId(), cancelMonitor); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java index b663267878..676918a154 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java @@ -24,7 +24,9 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -37,9 +39,7 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -48,6 +48,7 @@ import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.file.PathTranslationService; +import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchProjectBranchParams; @@ -82,11 +83,12 @@ public class ShowFixSuggestionRequestHandler implements HttpRequestHandler { private final PathTranslationService pathTranslationService; private final String sonarCloudUrl; private final SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService; + private final ClientFileSystemService clientFs; public ShowFixSuggestionRequestHandler(SonarLintRpcClient client, TelemetryService telemetryService, InitializeParams params, RequestHandlerBindingAssistant requestHandlerBindingAssistant, PathTranslationService pathTranslationService, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, - SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService) { + SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService, ClientFileSystemService clientFs) { this.client = client; this.telemetryService = telemetryService; this.canOpenFixSuggestion = params.getFeatureFlags().canOpenFixSuggestion(); @@ -94,12 +96,16 @@ public ShowFixSuggestionRequestHandler(SonarLintRpcClient client, TelemetryServi this.pathTranslationService = pathTranslationService; this.sonarCloudUrl = sonarCloudActiveEnvironment.getUri().toString(); this.sonarProjectBranchesSynchronizationService = sonarProjectBranchesSynchronizationService; + this.clientFs = clientFs; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { - var showFixSuggestionQuery = extractQuery(request); - if (!canOpenFixSuggestion || !Method.POST.isSame(request.getMethod()) || !showFixSuggestionQuery.isValid()) { + var originHeader = request.getHeader("Origin"); + var origin = originHeader != null ? originHeader.getValue() : null; + var showFixSuggestionQuery = extractQuery(request, origin); + + if (origin == null || !canOpenFixSuggestion || !Method.POST.isSame(request.getMethod()) || !showFixSuggestionQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } @@ -111,20 +117,29 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync( serverConnectionParams, - showFixSuggestionQuery.projectKey, - (connectionId, configScopeId, cancelMonitor) -> { + showFixSuggestionQuery.projectKey, origin, + (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { var branchToMatch = showFixSuggestionQuery.branch; if (branchToMatch == null) { - branchToMatch = sonarProjectBranchesSynchronizationService.findMainBranch(connectionId, showFixSuggestionQuery.projectKey, cancelMonitor); + branchToMatch = sonarProjectBranchesSynchronizationService.findMainBranch(connectionId, showFixSuggestionQuery.projectKey, + cancelMonitor); } - var localBranchMatchesRequesting = client.matchProjectBranch(new MatchProjectBranchParams(configScopeId, branchToMatch)).join().isBranchMatched(); + var localBranchMatchesRequesting = + client.matchProjectBranch(new MatchProjectBranchParams(configScopeId, branchToMatch)).join().isBranchMatched(); if (!localBranchMatchesRequesting) { - client.showMessage(new ShowMessageParams(MessageType.ERROR, "Attempted to show a fix suggestion from branch '" + branchToMatch + "', " + - "which is different from the currently checked-out branch.\nPlease switch to the correct branch and try again.")); + client.showMessage(new ShowMessageParams(MessageType.ERROR, + "Attempted to show a fix suggestion from branch '" + branchToMatch + "', which is different from the currently " + + "checked-out branch.\nPlease switch to the correct branch and try again.")); return; } - showFixSuggestionForScope(configScopeId, showFixSuggestionQuery.issueKey, showFixSuggestionQuery.fixSuggestion); + + if (doesClientFileExists(configScopeId, showFixSuggestionQuery.fixSuggestion.fileEdit.path, boundScopes)) { + showFixSuggestionForScope(configScopeId, showFixSuggestionQuery.issueKey, showFixSuggestionQuery.fixSuggestion); + } else { + client.showMessage(new ShowMessageParams(MessageType.ERROR, "Attempted to show a fix suggestion for a file that is " + + "not known by SonarQube for IDE")); + } } }); @@ -132,6 +147,22 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt response.setEntity(new StringEntity("OK")); } + private boolean doesClientFileExists(String configScopeId, String filePath, Collection boundScopes) { + var optTranslation = pathTranslationService.getOrComputePathTranslation(configScopeId); + if (optTranslation.isPresent()) { + var translation = optTranslation.get(); + var idePath = translation.serverToIdePath(Paths.get(filePath)); + for (var scope: boundScopes) { + for (var file: clientFs.getFiles(scope)) { + if (Path.of(file.getUri()).endsWith(idePath)) { + return true; + } + } + } + } + return false; + } + private static AssistCreatingConnectionParams createAssistServerConnectionParams(ShowFixSuggestionQuery query) { String tokenName = query.getTokenName(); String tokenValue = query.getTokenValue(); @@ -140,9 +171,8 @@ private static AssistCreatingConnectionParams createAssistServerConnectionParams : new AssistCreatingConnectionParams(new SonarQubeConnectionParams(query.getServerUrl(), tokenName, tokenValue)); } - private boolean isSonarCloud(ClassicHttpRequest request) throws ProtocolException { - return Optional.ofNullable(request.getHeader("Origin")) - .map(NameValuePair::getValue) + private boolean isSonarCloud(@Nullable String origin) { + return Optional.ofNullable(origin) .map(sonarCloudUrl::equals) .orElse(false); } @@ -167,7 +197,7 @@ private void showFixSuggestionForScope(String configScopeId, String issueKey, Fi } @VisibleForTesting - ShowFixSuggestionQuery extractQuery(ClassicHttpRequest request) throws HttpException, IOException { + ShowFixSuggestionQuery extractQuery(ClassicHttpRequest request, @Nullable String origin) throws HttpException, IOException { var params = new HashMap(); try { new URIBuilder(request.getUri(), StandardCharsets.UTF_8) @@ -177,7 +207,7 @@ ShowFixSuggestionQuery extractQuery(ClassicHttpRequest request) throws HttpExcep // Ignored } var payload = extractAndValidatePayload(request); - boolean isSonarCloud = isSonarCloud(request); + boolean isSonarCloud = isSonarCloud(origin); var serverUrl = isSonarCloud ? sonarCloudUrl : params.get("server"); return new ShowFixSuggestionQuery(serverUrl, params.get("project"), params.get("issue"), params.get("branch"), params.get("tokenName"), params.get("tokenValue"), params.get("organizationKey"), isSonarCloud, payload); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java index c9e937ab43..1b4fbab191 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java @@ -73,16 +73,18 @@ public ShowHotspotRequestHandler(SonarLintRpcClient client, ServerApiProvider se @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { + var originHeader = request.getHeader("Origin"); + var origin = originHeader != null ? originHeader.getValue() : null; var showHotspotQuery = extractQuery(request); - if (!Method.GET.isSame(request.getMethod()) || !showHotspotQuery.isValid()) { + if (origin == null || !Method.GET.isSame(request.getMethod()) || !showHotspotQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } telemetryService.showHotspotRequestReceived(); var sonarQubeConnectionParams = new SonarQubeConnectionParams(showHotspotQuery.serverUrl, null, null); var connectionParams = new AssistCreatingConnectionParams(sonarQubeConnectionParams); - requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync(connectionParams, showHotspotQuery.projectKey, - (connectionId, configScopeId, cancelMonitor) -> { + requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync(connectionParams, showHotspotQuery.projectKey, origin, + (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { showHotspotForScope(connectionId, configScopeId, showHotspotQuery.hotspotKey, cancelMonitor); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java index 0c0ea1a25e..ec0c83a8e3 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java @@ -94,8 +94,10 @@ public ShowIssueRequestHandler(SonarLintRpcClient client, ServerApiProvider serv @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { + var originHeader = request.getHeader("Origin"); + var origin = originHeader != null ? originHeader.getValue() : null; var showIssueQuery = extractQuery(request); - if (!Method.GET.isSame(request.getMethod()) || !showIssueQuery.isValid()) { + if (origin == null || !Method.GET.isSame(request.getMethod()) || !showIssueQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } @@ -106,7 +108,8 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync( serverConnectionParams, showIssueQuery.projectKey, - (connectionId, configScopeId, cancelMonitor) -> { + origin, + (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { var branchToMatch = showIssueQuery.branch; if (branchToMatch == null) { diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java index d8071f8799..4bef2fab34 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandlerTests.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Path; import java.util.List; import java.util.Optional; import java.util.Set; @@ -46,6 +47,8 @@ import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; +import org.sonarsource.sonarlint.core.fs.ClientFile; +import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; @@ -79,6 +82,8 @@ class ShowFixSuggestionRequestHandlerTests { private InitializeParams initializeParams; private ShowFixSuggestionRequestHandler showFixSuggestionRequestHandler; private TelemetryService telemetryService; + private ClientFile clientFile; + private FilePathTranslation filePathTranslation; @BeforeEach void setup() { @@ -87,7 +92,7 @@ void setup() { var bindingSuggestionProvider = mock(BindingSuggestionProvider.class); var bindingCandidatesFinder = mock(BindingCandidatesFinder.class); sonarLintRpcClient = mock(SonarLintRpcClient.class); - var filePathTranslation = mock(FilePathTranslation.class); + filePathTranslation = mock(FilePathTranslation.class); var pathTranslationService = mock(PathTranslationService.class); when(pathTranslationService.getOrComputePathTranslation(any())).thenReturn(Optional.of(filePathTranslation)); var userTokenService = mock(UserTokenService.class); @@ -99,10 +104,16 @@ void setup() { telemetryService = mock(TelemetryService.class); var sonarProjectBranchesSynchronizationService = mock(SonarProjectBranchesSynchronizationService.class); when(sonarProjectBranchesSynchronizationService.getProjectBranches(any(), any(), any())).thenReturn(new ProjectBranches(Set.of(), "main")); + clientFile = mock(ClientFile.class); + var clientFs = mock(ClientFileSystemService.class); + when(clientFs.getFiles(any())).thenReturn(List.of(clientFile)); + var connectionConfiguration = mock(ConnectionConfigurationRepository.class); + when(connectionConfiguration.hasConnectionWithOrigin(PRODUCTION_URI.toString())).thenReturn(true); showFixSuggestionRequestHandler = new ShowFixSuggestionRequestHandler(sonarLintRpcClient, telemetryService, initializeParams, new RequestHandlerBindingAssistant(bindingSuggestionProvider, bindingCandidatesFinder, sonarLintRpcClient, connectionConfigurationRepository, - configurationRepository, userTokenService, sonarCloudActiveEnvironment), pathTranslationService, sonarCloudActiveEnvironment, sonarProjectBranchesSynchronizationService); + configurationRepository, userTokenService, sonarCloudActiveEnvironment, connectionConfiguration), pathTranslationService, + sonarCloudActiveEnvironment, sonarProjectBranchesSynchronizationService, clientFs); } @Test @@ -162,7 +173,7 @@ void should_extract_query_from_sc_request_without_token() throws HttpException, "\"suggestionId\": \"eb93b2b4-f7b0-4b5c-9460-50893968c264\",\n" + "\"explanation\": \"Modifying the variable name is good\"\n" + "}\n")); - var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request); + var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request, request.getHeader("Origin").getValue()); assertThat(showFixSuggestionQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(showFixSuggestionQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(showFixSuggestionQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); @@ -204,7 +215,7 @@ void should_extract_query_from_sc_request_with_token() throws HttpException, IOE "\"suggestionId\": \"eb93b2b4-f7b0-4b5c-9460-50893968c264\",\n" + "\"explanation\": \"Modifying the variable name is good\"\n" + "}\n")); - var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request); + var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request, request.getHeader("Origin").getValue()); assertThat(showFixSuggestionQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(showFixSuggestionQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(showFixSuggestionQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); @@ -294,6 +305,8 @@ void should_find_main_branch_when_not_provided_and_not_stored() throws HttpExcep var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); + when(clientFile.getUri()).thenReturn(URI.create("file:///src/main/java/Main.java")); + when(filePathTranslation.serverToIdePath(any())).thenReturn(Path.of("src/main/java/Main.java")); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( new SonarCloudConnectionConfiguration(PRODUCTION_URI, "name", "organizationKey", false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); @@ -304,6 +317,35 @@ void should_find_main_branch_when_not_provided_and_not_stored() throws HttpExcep await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> verify(sonarLintRpcClient).showFixSuggestion(any())); } + @Test + void should_verify_missing_origin() throws HttpException, IOException { + var request = new BasicClassicHttpRequest("POST", "/sonarlint/api/fix/show" + + "?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + + "&issue=AX2VL6pgAvx3iwyNtLyr&branch=branch" + + "&organizationKey=sample-organization"); + request.setEntity(new StringEntity("{\n" + + "\"fileEdit\": {\n" + + "\"path\": \"src/main/java/Main.java\",\n" + + "\"changes\": [{\n" + + "\"beforeLineRange\": {\n" + + "\"startLine\": 0,\n" + + "\"endLine\": 1\n" + + "},\n" + + "\"before\": \"\",\n" + + "\"after\": \"var fix = 1;\"\n" + + "}]\n" + + "},\n" + + "\"suggestionId\": \"eb93b2b4-f7b0-4b5c-9460-50893968c264\",\n" + + "\"explanation\": \"Modifying the variable name is good\"\n" + + "}\n")); + var response = mock(ClassicHttpResponse.class); + var context = mock(HttpContext.class); + + showFixSuggestionRequestHandler.handle(request, response, context); + + verifyNoMoreInteractions(sonarLintRpcClient); + } + private static ShowFixSuggestionRequestHandler.FixSuggestionPayload generateFixSuggestionPayload() { return new ShowFixSuggestionRequestHandler.FixSuggestionPayload( new ShowFixSuggestionRequestHandler.FileEditPayload( diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java index c7a3fc0d8b..f1e4672c91 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java @@ -35,6 +35,7 @@ import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -124,11 +125,13 @@ void setup() { any()); when(storageService.binding(any())).thenReturn(sonarStorage); when(sonarStorage.branches()).thenReturn(branchesStorage); + var connectionConfiguration = mock(ConnectionConfigurationRepository.class); + when(connectionConfiguration.hasConnectionWithOrigin(PRODUCTION_URI.toString())).thenReturn(true); showIssueRequestHandler = spy(new ShowIssueRequestHandler(sonarLintRpcClient, serverApiProvider, telemetryService, new RequestHandlerBindingAssistant(bindingSuggestionProvider, bindingCandidatesFinder, sonarLintRpcClient, connectionConfigurationRepository, configurationRepository, userTokenService, - sonarCloudActiveEnvironment), pathTranslationService, sonarCloudActiveEnvironment, sonarProjectBranchesSynchronizationService)); + sonarCloudActiveEnvironment, connectionConfiguration), pathTranslationService, sonarCloudActiveEnvironment, sonarProjectBranchesSynchronizationService)); } @Test @@ -207,6 +210,7 @@ void should_transform_ServerIssueDetail_to_ShowIssueParams() { void should_trigger_telemetry() throws HttpException, IOException, URISyntaxException { var request = mock(ClassicHttpRequest.class); when(request.getUri()).thenReturn(URI.create("http://localhost:8000/issue?project=pk&issue=ik&branch=b&server=s")); + when(request.getHeader("Origin")).thenReturn(new BasicHeader("Origin", "s")); when(request.getMethod()).thenReturn(Method.GET.name()); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); @@ -436,4 +440,20 @@ void should_find_main_branch_when_not_provided_and_not_stored() throws HttpExcep await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> verify(sonarLintRpcClient).showIssue(any())); } + @Test + void should_verify_missing_origin() throws HttpException, IOException { + var request = new BasicClassicHttpRequest("GET", "/sonarlint/api/issues/show" + + "?server=https%3A%2F%2Fnext.sonarqube.com%2Fsonarqube" + + "&project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + + "&issue=AX2VL6pgAvx3iwyNtLyr&tokenName=abc" + + "&organizationKey=sample-organization" + + "&tokenValue=123"); + var response = mock(ClassicHttpResponse.class); + var context = mock(HttpContext.class); + + showIssueRequestHandler.handle(request, response, context); + + verifyNoMoreInteractions(sonarLintRpcClient); + } + } diff --git a/medium-tests/src/test/java/mediumtest/analysis/AnalysysForcedByClientMediumTests.java b/medium-tests/src/test/java/mediumtest/analysis/AnalysysForcedByClientMediumTests.java index f739502398..d3d683e3fb 100644 --- a/medium-tests/src/test/java/mediumtest/analysis/AnalysysForcedByClientMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/analysis/AnalysysForcedByClientMediumTests.java @@ -32,6 +32,7 @@ import org.assertj.core.api.Assertions; import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -167,6 +168,7 @@ void should_run_forced_analysis_vcs_changed_files(@TempDir Path baseDir) throws } @Test + @Disabled("Flaky tests") void should_run_forced_full_project_analysis_only_for_hotspots(@TempDir Path baseDir) { var fileFoo = createFile(baseDir, "Foo.java", "public class Foo {\n" + "\n" + @@ -221,6 +223,7 @@ void should_run_forced_full_project_analysis_only_for_hotspots(@TempDir Path bas } @Test + @Disabled("Flaky test") void should_run_forced_full_project_analysis_for_all_findings(@TempDir Path baseDir) { var fileFoo = createFile(baseDir, "Foo.java", "public class Foo {\n" + "\n" + diff --git a/medium-tests/src/test/java/mediumtest/hotspots/OpenHotspotInIdeMediumTests.java b/medium-tests/src/test/java/mediumtest/hotspots/OpenHotspotInIdeMediumTests.java index 5075ecd06a..603e350fef 100644 --- a/medium-tests/src/test/java/mediumtest/hotspots/OpenHotspotInIdeMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/hotspots/OpenHotspotInIdeMediumTests.java @@ -227,7 +227,8 @@ void it_should_display_a_message_when_failing_to_fetch_the_hotspot() { .withEmbeddedServer() .build(fakeClient); - var statusCode = requestGetOpenHotspotWithParams("server=" + urlEncode(serverWithoutHotspot.baseUrl()) + "&project=projectKey&hotspot=key"); + var statusCode = requestGetOpenHotspotWithParams("server=" + urlEncode(serverWithoutHotspot.baseUrl()) + "&project=projectKey&hotspot=key", + serverWithoutHotspot.baseUrl()); assertThat(statusCode).isEqualTo(200); verify(fakeClient, timeout(2000)).showMessage(MessageType.ERROR, "Could not show the hotspot. See logs for more details"); @@ -248,25 +249,28 @@ void it_should_not_accept_post_method() { assertThat(statusCode).isEqualTo(400); } + private int requestGetOpenHotspotWithParams(String query, String baseUrl) { + return requestOpenHotspotWithParams(query, "GET", baseUrl, HttpRequest.BodyPublishers.noBody()); + } + private int requestGetOpenHotspotWithParams(String query) { - return requestOpenHotspotWithParams(query, "GET", HttpRequest.BodyPublishers.noBody()); + return requestOpenHotspotWithParams(query, "GET", serverWithHotspot.baseUrl(), HttpRequest.BodyPublishers.noBody()); } private int requestPostOpenHotspotWithParams(String query) { - return requestOpenHotspotWithParams(query, "POST", HttpRequest.BodyPublishers.ofString("")); + return requestOpenHotspotWithParams(query, "POST", serverWithHotspot.baseUrl(), HttpRequest.BodyPublishers.ofString("")); } - private int requestOpenHotspotWithParams(String query, String method, HttpRequest.BodyPublisher bodyPublisher) { + private int requestOpenHotspotWithParams(String query, String method, String baseUrl, HttpRequest.BodyPublisher bodyPublisher) { var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/hotspots/show?" + query)) + .header("Origin", baseUrl) .method(method, bodyPublisher) .build(); - HttpResponse response = null; + HttpResponse response; try { response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { + } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } return response.statusCode(); diff --git a/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java b/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java index 0f8af883b5..2c28d57784 100644 --- a/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; @@ -48,6 +49,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; +import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; import static mediumtest.fixtures.ServerFixture.newSonarCloudServer; import static mediumtest.fixtures.SonarLintBackendFixture.newBackend; @@ -63,6 +65,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static testutils.AnalysisUtils.createFile; class OpenFixSuggestionInIdeMediumTests { @@ -77,7 +80,7 @@ class OpenFixSuggestionInIdeMediumTests { private static final String ORG_KEY = "orgKey"; private static final String FIX_PAYLOAD = "{\n" + "\"fileEdit\": {\n" + - "\"path\": \"src/main/java/Main.java\",\n" + + "\"path\": \"Main.java\",\n" + "\"changes\": [{\n" + "\"beforeLineRange\": {\n" + "\"startLine\": 0,\n" + @@ -106,8 +109,11 @@ void tearDown() throws ExecutionException, InterruptedException { } @Test - void it_should_update_the_telemetry_on_show_issue() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_update_the_telemetry_on_show_issue(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); backend = newBackend() .withSonarCloudUrl(scServer.baseUrl()) .withBoundConfigScope(CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY) @@ -130,8 +136,11 @@ void it_should_update_the_telemetry_on_show_issue() throws Exception { } @Test - void it_should_open_a_fix_suggestion_in_ide() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_open_a_fix_suggestion_in_ide(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); backend = newBackend() .withSonarCloudUrl(scServer.baseUrl()) .withSonarCloudConnection(CONNECTION_ID, ORG_KEY) @@ -152,7 +161,7 @@ void it_should_open_a_fix_suggestion_in_ide() throws Exception { assertThat(fixSuggestion).isNotNull(); assertThat(fixSuggestion.suggestionId()).isEqualTo("eb93b2b4-f7b0-4b5c-9460-50893968c264"); assertThat(fixSuggestion.explanation()).isEqualTo("Modifying the variable name is good"); - assertThat(fixSuggestion.fileEdit().idePath().toString()).contains(pathTranslation.serverToIdePath(Paths.get("src/main/java/Main.java")).toString()); + assertThat(fixSuggestion.fileEdit().idePath().toString()).contains(pathTranslation.serverToIdePath(Paths.get("Main.java")).toString()); assertThat(fixSuggestion.fileEdit().changes()).hasSize(1); var change = fixSuggestion.fileEdit().changes().get(0); assertThat(change.before()).isEmpty(); @@ -162,8 +171,11 @@ void it_should_open_a_fix_suggestion_in_ide() throws Exception { } @Test - void it_should_assist_creating_the_binding_if_scope_not_bound() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_assist_creating_the_binding_if_scope_not_bound(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); mockAssistCreatingConnection(fakeClient, CONNECTION_ID); mockAssistBinding(fakeClient, CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY); @@ -182,8 +194,11 @@ void it_should_assist_creating_the_binding_if_scope_not_bound() throws Exception } @Test - void it_should_not_assist_binding_if_multiple_suggestions() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_not_assist_binding_if_multiple_suggestions(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); mockAssistCreatingConnection(fakeClient, CONNECTION_ID); mockAssistBinding(fakeClient, CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY); backend = newBackend() @@ -203,8 +218,11 @@ void it_should_not_assist_binding_if_multiple_suggestions() throws Exception { } @Test - void it_should_assist_binding_if_multiple_suggestions_but_scopes_are_parent_and_child() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_assist_binding_if_multiple_suggestions_but_scopes_are_parent_and_child(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs("configScopeParent", List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), "configScopeParent", false, null, inputFile, null, null, true))) + .build(); mockAssistCreatingConnection(fakeClient, CONNECTION_ID); mockAssistBinding(fakeClient, "configScopeParent", CONNECTION_ID, PROJECT_KEY); backend = newBackend() @@ -223,8 +241,11 @@ void it_should_assist_binding_if_multiple_suggestions_but_scopes_are_parent_and_ } @Test - void it_should_assist_creating_the_connection_when_no_sc_connection() throws Exception { - var fakeClient = newFakeClient().build(); + void it_should_assist_creating_the_connection_when_no_sc_connection(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); mockAssistCreatingConnection(fakeClient, CONNECTION_ID); mockAssistBinding(fakeClient, CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY); @@ -306,6 +327,62 @@ void it_should_fail_request_when_project_parameter_missing() throws IOException, assertThat(statusCode).isEqualTo(400); } + @Test + void it_should_fail_when_origin_is_missing() throws IOException, InterruptedException { + var fakeClient = newFakeClient().build(); + backend = newBackend() + .withSonarCloudUrl(scServer.baseUrl()) + .withSonarCloudConnection(CONNECTION_ID, ORG_KEY) + .withBoundConfigScope(CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY) + .withEmbeddedServer() + .withOpenFixSuggestion() + .build(fakeClient); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + "http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/fix/show?server=" + scServer.baseUrl() + "&issue=" + ISSUE_KEY + + "&project=" + PROJECT_KEY + "&branch=" + BRANCH_NAME + "&organizationKey=" + ORG_KEY + )) + .POST(HttpRequest.BodyPublishers.ofString(FIX_PAYLOAD)).build(); + var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + + var statusCode = response.statusCode(); + + assertThat(statusCode).isEqualTo(400); + } + + @Test + void it_should_fail_when_origin_does_not_match(@TempDir Path baseDir) throws Exception { + var inputFile = createFile(baseDir, "Main.java", ""); + var fakeClient = newFakeClient() + .withInitialFs(CONFIG_SCOPE_ID, List.of(new ClientFileDto(inputFile.toUri(), baseDir.relativize(inputFile), CONFIG_SCOPE_ID, false, null, inputFile, null, null, true))) + .build(); + backend = newBackend() + .withSonarCloudUrl(scServer.baseUrl()) + .withSonarCloudConnection(CONNECTION_ID, ORG_KEY) + .withBoundConfigScope(CONFIG_SCOPE_ID, CONNECTION_ID, PROJECT_KEY) + .withEmbeddedServer() + .withOpenFixSuggestion() + .build(fakeClient); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + "http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/fix/show?server=" + scServer.baseUrl() + "&issue=" + ISSUE_KEY + + "&project=" + PROJECT_KEY + "&branch=" + BRANCH_NAME + "&organizationKey=" + ORG_KEY + )) + .header("Origin", "malicious") + .POST(HttpRequest.BodyPublishers.ofString(FIX_PAYLOAD)).build(); + var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + + var statusCode = response.statusCode(); + + assertThat(statusCode).isEqualTo(200); + + ArgumentCaptor captor = ArgumentCaptor.captor(); + verify(fakeClient, after(500).atLeastOnce()).log(captor.capture()); + assertThat(captor.getAllValues()) + .extracting(LogParams::getMessage) + .containsAnyOf("The origin 'malicious' is not trusted, this could be a malicious request"); + } + private Object executeOpenFixSuggestionRequestWithToken(String payload, String issueKey, String projectKey, String branchName, String orgKey, String tokenName, String tokenValue) throws IOException, InterruptedException { HttpRequest request = openFixSuggestionRequest(payload, "&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branchName, diff --git a/medium-tests/src/test/java/mediumtest/issues/OpenIssueInIdeMediumTests.java b/medium-tests/src/test/java/mediumtest/issues/OpenIssueInIdeMediumTests.java index 3c68d3ea48..685c969fcd 100644 --- a/medium-tests/src/test/java/mediumtest/issues/OpenIssueInIdeMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/issues/OpenIssueInIdeMediumTests.java @@ -392,34 +392,34 @@ void it_should_fail_request_when_project_parameter_missing() throws Exception { } private int executeOpenIssueRequest(String issueKey, String projectKey, String branch) throws IOException, InterruptedException { - HttpRequest request = openIssueRequest("&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch); + HttpRequest request = openIssueRequest(serverWithIssues.baseUrl(), "&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch); var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return response.statusCode(); } private int executeOpenSCIssueRequest(String issueKey, String projectKey, String branch, String organizationKey) throws IOException, InterruptedException { - HttpRequest request = this.openIssueRequest("&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch, "&organizationKey=" + organizationKey); + HttpRequest request = this.openIssueRequest("https://sonar.my", "&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch, "&organizationKey=" + organizationKey); var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return response.statusCode(); } private Object executeOpenSCIssueRequest(String issueKey, String projectKey, String branchName, String orgKey, String tokenName, String tokenValue) throws IOException, InterruptedException { - HttpRequest request = this.openIssueRequest("&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branchName, "&organizationKey=" + orgKey, "&tokenName=" + tokenName, "&tokenValue=" + tokenValue); + HttpRequest request = this.openIssueRequest("https://sonar.my", "&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branchName, "&organizationKey=" + orgKey, "&tokenName=" + tokenName, "&tokenValue=" + tokenValue); var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return response.statusCode(); } private int executeOpenIssueRequest(String issueKey, String projectKey, String branch, String pullRequest) throws IOException, InterruptedException { - HttpRequest request = openIssueRequest("&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch, "&pullRequest=" + pullRequest); + HttpRequest request = openIssueRequest(serverWithIssues.baseUrl(), "&issue=" + issueKey, "&project=" + projectKey, "&branch=" + branch, "&pullRequest=" + pullRequest); var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return response.statusCode(); } - private HttpRequest openIssueRequest(String... params) { + private HttpRequest openIssueRequest(String baseUrl, String... params) { return HttpRequest.newBuilder() .uri(URI.create( "http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/issues/show?server=" + serverWithIssues.baseUrl() + String.join("", params))) - .header("Origin", "https://sonar.my") + .header("Origin", baseUrl) .GET().build(); }