Skip to content

Commit

Permalink
SLCORE-433 Fix SSF-359
Browse files Browse the repository at this point in the history
  • Loading branch information
damien-urruty-sonarsource committed May 25, 2023
1 parent c6fd913 commit 562c142
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public CompletableFuture<HelpGenerateUserTokenResponse> helpGenerateUserToken(He
client.openUrlInBrowser(new OpenUrlInBrowserParams(ServerApiHelper.concat(serverBaseUrl, getUserTokenGenerationRelativeUrlToOpen(automaticTokenGenerationSupported))));
var shouldWaitIncomingToken = Boolean.TRUE.equals(automaticTokenGenerationSupported) && embeddedServer.isStarted();
if (shouldWaitIncomingToken) {
awaitingUserTokenFutureRepository.setFutureResponse(futureTokenResponse);
awaitingUserTokenFutureRepository.addExpectedResponse(serverBaseUrl, futureTokenResponse);
} else {
futureTokenResponse.complete(new HelpGenerateUserTokenResponse(null));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,31 @@

import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentHashMap;

import org.sonarsource.sonarlint.core.clientapi.backend.authentication.HelpGenerateUserTokenResponse;

import static org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository.haveSameOrigin;

public class AwaitingUserTokenFutureRepository {
private final AtomicReference<CompletableFuture<HelpGenerateUserTokenResponse>> futureResponse = new AtomicReference<>();
private final ConcurrentHashMap<String, CompletableFuture<HelpGenerateUserTokenResponse>> awaitingFuturesByServerUrl = new ConcurrentHashMap<>();

public void setFutureResponse(CompletableFuture<HelpGenerateUserTokenResponse> futureResponse) {
var previousFuture = this.futureResponse.getAndSet(futureResponse);
public void addExpectedResponse(String serverBaseUrl, CompletableFuture<HelpGenerateUserTokenResponse> futureResponse) {
var previousFuture = awaitingFuturesByServerUrl.put(serverBaseUrl, futureResponse);
if (previousFuture != null) {
previousFuture.cancel(false);
}
}

public Optional<CompletableFuture<HelpGenerateUserTokenResponse>> consumeFutureResponse() {
return Optional.ofNullable(futureResponse.getAndSet(null));
public Optional<CompletableFuture<HelpGenerateUserTokenResponse>> consumeFutureResponse(String serverOrigin) {
for (var iterator = awaitingFuturesByServerUrl.entrySet().iterator(); iterator.hasNext();) {
var entry = iterator.next();
if (haveSameOrigin(entry.getKey(), serverOrigin)) {
iterator.remove();
return Optional.of(entry.getValue());
}
}
return Optional.empty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,20 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt
return;
}

awaitingUserTokenFutureRepository.consumeFutureResponse()
var originHeader = request.getHeader("Origin");
var origin = originHeader != null ? originHeader.getValue() : null;
if (origin == null) {
response.setCode(HttpStatus.SC_BAD_REQUEST);
return;
}

awaitingUserTokenFutureRepository.consumeFutureResponse(origin)
.filter(not(CompletableFuture::isCancelled))
.ifPresent(pendingFuture -> pendingFuture.complete(new HelpGenerateUserTokenResponse(token)));
response.setCode(HttpStatus.SC_OK);
response.setEntity(new StringEntity("OK"));
.ifPresentOrElse(pendingFuture -> {
pendingFuture.complete(new HelpGenerateUserTokenResponse(token));
response.setCode(HttpStatus.SC_OK);
response.setEntity(new StringEntity("OK"));
}, () -> response.setCode(HttpStatus.SC_FORBIDDEN));
}

private static String extractAndValidateToken(ClassicHttpRequest request) throws IOException, ParseException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,17 @@ public boolean hasConnectionWithOrigin(String serverOrigin) {
// passed Origin
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
return connectionsById.values().stream()
.anyMatch(connection -> ensureTrailingSlash(connection.getUrl()).startsWith(ensureTrailingSlash(serverOrigin)));
.anyMatch(connection -> haveSameOrigin(connection.getUrl(), serverOrigin));
}

public static boolean haveSameOrigin(String knownServerUrl, String incomingOrigin) {
return ensureTrailingSlash(knownServerUrl).startsWith(ensureTrailingSlash(incomingOrigin));
}

private static String ensureTrailingSlash(String s) {
return !s.endsWith("/") ? (s + "/") : s;
}


public List<AbstractConnectionConfiguration> findByUrl(String serverUrl) {
return connectionsById.values().stream()
.filter(connection -> connection.isSameServerUrl(serverUrl))
Expand Down
47 changes: 44 additions & 3 deletions core/src/test/java/mediumtest/AuthenticationHelperMediumTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,19 @@
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.ExecutionException;

import mediumtest.fixtures.ServerFixture;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.sonarsource.sonarlint.core.SonarLintBackendImpl;
import org.sonarsource.sonarlint.core.clientapi.backend.authentication.HelpGenerateUserTokenParams;
import org.sonarsource.sonarlint.core.clientapi.backend.authentication.HelpGenerateUserTokenResponse;
import org.sonarsource.sonarlint.core.commons.http.HttpClient;

import static mediumtest.fixtures.ServerFixture.newSonarQubeServer;
import static mediumtest.fixtures.SonarLintBackendFixture.newBackend;
import static mediumtest.fixtures.SonarLintBackendFixture.newFakeClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.sonarsource.sonarlint.core.commons.testutils.MockWebServerExtension.httpClient;

class AuthenticationHelperMediumTests {

Expand Down Expand Up @@ -82,8 +81,8 @@ void it_should_open_the_security_url_for_sonarqube_older_than_9_7() {
@Test
void it_should_open_the_sonarlint_auth_url_for_sonarqube_9_7_plus() throws IOException, InterruptedException {
var fakeClient = newFakeClient().withHostName("ClientName").build();
backend = newBackend().withEmbeddedServer().build(fakeClient);
server = newSonarQubeServer("9.7").start();
backend = newBackend().withEmbeddedServer().withSonarQubeConnection("connectionId", server).build(fakeClient);

var futureResponse = backend.getAuthenticationHelperService().helpGenerateUserToken(new HelpGenerateUserTokenParams(server.baseUrl(), false));

Expand All @@ -94,6 +93,7 @@ void it_should_open_the_sonarlint_auth_url_for_sonarqube_9_7_plus() throws IOExc
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/token"))
.header("Content-Type", "application/json; charset=utf-8")
.header("Origin", server.baseUrl())
.POST(HttpRequest.BodyPublishers.ofString("{\"token\": \"value\"}")).build();
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
Expand All @@ -104,6 +104,47 @@ void it_should_open_the_sonarlint_auth_url_for_sonarqube_9_7_plus() throws IOExc
.isEqualTo("value");
}

@Test
void it_should_reject_tokens_from_missing_origin() throws IOException, InterruptedException {
var fakeClient = newFakeClient().withHostName("ClientName").build();
server = newSonarQubeServer("9.7").start();
backend = newBackend().withEmbeddedServer().withSonarQubeConnection("connectionId", server).build(fakeClient);

backend.getAuthenticationHelperService().helpGenerateUserToken(new HelpGenerateUserTokenParams(server.baseUrl(), false));

await().atMost(Duration.ofSeconds(3)).until(() -> !fakeClient.getUrlsToOpen().isEmpty());
assertThat(fakeClient.getUrlsToOpen())
.containsExactly(server.url("/sonarlint/auth?ideName=ClientName&port=" + backend.getEmbeddedServerPort()));

var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/token"))
.header("Content-Type", "application/json; charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString("{\"token\": \"value\"}")).build();
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(400);
}

@Test
void it_should_reject_tokens_from_unexpected_origin() throws IOException, InterruptedException {
var fakeClient = newFakeClient().withHostName("ClientName").build();
server = newSonarQubeServer("9.7").start();
backend = newBackend().withEmbeddedServer().withSonarQubeConnection("connectionId", server).build(fakeClient);

backend.getAuthenticationHelperService().helpGenerateUserToken(new HelpGenerateUserTokenParams(server.baseUrl(), false));

await().atMost(Duration.ofSeconds(3)).until(() -> !fakeClient.getUrlsToOpen().isEmpty());
assertThat(fakeClient.getUrlsToOpen())
.containsExactly(server.url("/sonarlint/auth?ideName=ClientName&port=" + backend.getEmbeddedServerPort()));

var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + backend.getEmbeddedServerPort() + "/sonarlint/api/token"))
.header("Content-Type", "application/json; charset=utf-8")
.header("Origin", "https://unexpected.sonar")
.POST(HttpRequest.BodyPublishers.ofString("{\"token\": \"value\"}")).build();
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(403);
}

@Test
void it_should_open_the_sonarlint_auth_url_without_port_for_sonarqube_9_7_plus_when_server_is_not_started() {
var fakeClient = newFakeClient().withHostName("ClientName").build();
Expand Down

0 comments on commit 562c142

Please sign in to comment.