Skip to content

Commit 4270be0

Browse files
committed
SCANJLIB-262 Restore support of token authentication for SonarQube 9.9
* Add integration tests for the various cases of authentication, including SONAR_TOKEN * Accept more than 404 errors on the first call to /analysis/version
1 parent 8680eb6 commit 4270be0

File tree

8 files changed

+220
-24
lines changed

8 files changed

+220
-24
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* SonarScanner Java Library - ITs
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package com.sonar.scanner.lib.it;
21+
22+
import com.sonar.orchestrator.build.BuildResult;
23+
import com.sonar.orchestrator.container.Server;
24+
import com.sonar.orchestrator.junit4.OrchestratorRule;
25+
import com.sonar.scanner.lib.it.tools.SimpleScanner;
26+
import java.io.IOException;
27+
import java.nio.file.Path;
28+
import java.nio.file.Paths;
29+
import java.util.Map;
30+
import org.junit.Assume;
31+
import org.junit.ClassRule;
32+
import org.junit.Test;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
public class AuthenticationTest {
37+
38+
@ClassRule
39+
public static final OrchestratorRule ORCHESTRATOR = ScannerJavaLibraryTestSuite.ORCHESTRATOR;
40+
41+
@Test
42+
public void useTokenAuthenticationByProperties() throws IOException {
43+
SimpleScanner scanner = new SimpleScanner();
44+
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), ORCHESTRATOR.getServer().getUrl(), Map.of(
45+
ScannerJavaLibraryTestSuite.ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 0) ? "sonar.token" : "sonar.login",
46+
ScannerJavaLibraryTestSuite.ORCHESTRATOR.getDefaultAdminToken()
47+
), Map.of(), false);
48+
assertThat(buildResult.getLastStatus()).isZero();
49+
}
50+
51+
@Test
52+
public void useTokenAuthenticationByEnvVariable() throws IOException {
53+
SimpleScanner scanner = new SimpleScanner();
54+
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), ORCHESTRATOR.getServer().getUrl(), Map.of(),
55+
Map.of("SONAR_TOKEN", ScannerJavaLibraryTestSuite.ORCHESTRATOR.getDefaultAdminToken()), false);
56+
assertThat(buildResult.getLastStatus()).isZero();
57+
}
58+
59+
@Test
60+
public void useLoginPasswordAuthentication() throws IOException {
61+
// Support of login/password authentication has been dropped in SQS 25.1
62+
Assume.assumeFalse(ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(25, 1));
63+
SimpleScanner scanner = new SimpleScanner();
64+
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), ORCHESTRATOR.getServer().getUrl(), Map.of(
65+
"sonar.login", Server.ADMIN_LOGIN,
66+
"sonar.password", Server.ADMIN_PASSWORD
67+
), Map.of(), false);
68+
assertThat(buildResult.getLastStatus()).isZero();
69+
}
70+
71+
private static Path project(String projectName) {
72+
return Paths.get("..", "projects", projectName);
73+
}
74+
75+
76+
}

its/it-tests/src/test/java/com/sonar/scanner/lib/it/ProxyTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ public void simple_analysis_with_proxy_auth() throws Exception {
207207

208208
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), ORCHESTRATOR.getServer().getUrl(), params, Map.of());
209209
assertThat(buildResult.getLastStatus()).isNotZero();
210-
assertThat(buildResult.getLogs()).contains("Failed to query server version: Proxy Authentication Required.");
210+
assertThat(buildResult.getLogs()).contains("Failed to query server version: HTTP 407 Proxy Authentication Required.");
211211
assertThat(seenByProxy).isEmpty();
212212

213213
params.put("sonar.scanner.proxyUser", PROXY_USER);

its/it-tests/src/test/java/com/sonar/scanner/lib/it/ScannerJavaLibraryTestSuite.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.junit.runners.Suite.SuiteClasses;
3232

3333
@RunWith(Suite.class)
34-
@SuiteClasses({ProxyTest.class, SSLTest.class, PropertiesTest.class})
34+
@SuiteClasses({AuthenticationTest.class, ProxyTest.class, SSLTest.class, PropertiesTest.class})
3535
public class ScannerJavaLibraryTestSuite {
3636
private static final String SONAR_RUNTIME_VERSION = "sonar.runtimeVersion";
3737

@@ -41,6 +41,7 @@ public class ScannerJavaLibraryTestSuite {
4141
.setSonarVersion(getServerVersion())
4242
.setEdition(getServerVersion().equals(LATEST_RELEASE) ? Edition.COMMUNITY : Edition.DEVELOPER)
4343
.useDefaultAdminCredentialsForBuilds(true)
44+
.defaultForceAuthentication()
4445
.addBundledPluginToKeep("sonar-javascript")
4546
.build();
4647

its/it-tests/src/test/java/com/sonar/scanner/lib/it/tools/SimpleScanner.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package com.sonar.scanner.lib.it.tools;
2121

2222
import com.sonar.orchestrator.build.BuildResult;
23-
import com.sonar.orchestrator.container.Server;
2423
import com.sonar.scanner.lib.it.ScannerJavaLibraryTestSuite;
2524
import java.io.IOException;
2625
import java.nio.file.Files;
@@ -55,8 +54,12 @@ public BuildResult executeSimpleProject(Path baseDir, String host) throws IOExce
5554
}
5655

5756
public BuildResult executeSimpleProject(Path baseDir, String host, Map<String, String> extraProps, Map<String, String> env) throws IOException {
57+
return executeSimpleProject(baseDir, host, extraProps, env, true);
58+
}
59+
60+
public BuildResult executeSimpleProject(Path baseDir, String host, Map<String, String> extraProps, Map<String, String> env, boolean configureAuth) throws IOException {
5861
List<String> params = new ArrayList<>();
59-
Map<String, String> props = getSimpleProjectProperties(baseDir, host, extraProps);
62+
Map<String, String> props = getSimpleProjectProperties(baseDir, host, extraProps, configureAuth);
6063

6164
props.forEach((k, v) -> params.add("-D" + k + "=" + v));
6265
params.add("-jar");
@@ -70,19 +73,21 @@ public BuildResult executeSimpleProject(Path baseDir, String host, Map<String, S
7073
return result;
7174
}
7275

73-
private Map<String, String> getSimpleProjectProperties(Path baseDir, String host, Map<String, String> extraProps) throws IOException {
76+
private Map<String, String> getSimpleProjectProperties(Path baseDir, String host, Map<String, String> extraProps, boolean configureAuth) throws IOException {
7477
Properties analysisProperties = new Properties();
7578
Path propertiesFile = baseDir.resolve("sonar-project.properties");
7679
analysisProperties.load(Files.newInputStream(propertiesFile));
7780
analysisProperties.setProperty("sonar.projectBaseDir", baseDir.toAbsolutePath().toString());
7881
analysisProperties.setProperty("sonar.host.url", host);
79-
if (ScannerJavaLibraryTestSuite.ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 0)) {
80-
analysisProperties.setProperty("sonar.token", ScannerJavaLibraryTestSuite.ORCHESTRATOR.getDefaultAdminToken());
81-
} else {
82-
analysisProperties.setProperty("sonar.login", Server.ADMIN_LOGIN);
83-
analysisProperties.setProperty("sonar.password", Server.ADMIN_PASSWORD);
82+
if (configureAuth) {
83+
if (ScannerJavaLibraryTestSuite.ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 0)) {
84+
analysisProperties.setProperty("sonar.token", ScannerJavaLibraryTestSuite.ORCHESTRATOR.getDefaultAdminToken());
85+
} else {
86+
analysisProperties.setProperty("sonar.login", ScannerJavaLibraryTestSuite.ORCHESTRATOR.getDefaultAdminToken());
87+
}
8488
}
8589
analysisProperties.putAll(extraProps);
8690
return (Map) analysisProperties;
8791
}
92+
8893
}

lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,20 +262,37 @@ private static Path resolveSonarUserHome(Map<String, String> properties) {
262262
private static String getServerVersion(ScannerHttpClient scannerHttpClient) {
263263
try {
264264
return scannerHttpClient.callRestApi("/analysis/version");
265-
} catch (Exception e) {
266-
if (e instanceof HttpException && ((HttpException) e).getCode() == 404) {
267-
// Fallback to the old endpoint
268-
try {
269-
return scannerHttpClient.callWebApi("/api/server/version");
270-
} catch (Exception e2) {
271-
var ex = new MessageException("Failed to query server version: " + e2.getMessage(), e2);
272-
ex.addSuppressed(e);
273-
throw ex;
265+
} catch (HttpException httpException) {
266+
// Fallback to the old endpoint
267+
try {
268+
var serverVersion = scannerHttpClient.callWebApi("/api/server/version");
269+
if (VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) {
270+
// If version is greater than 10.6, we would have expected the first call to succeed, so it is better to throw the original exception than moving on
271+
// and having the scanner failing later (usually because of authentication issues)
272+
throw httpException;
274273
}
275-
} else {
276-
throw new MessageException("Failed to query server version: " + e.getMessage(), e);
274+
return serverVersion;
275+
} catch (Exception e2) {
276+
var ex = new MessageException("Failed to query server version: " + formatMessage(e2), e2);
277+
if (!e2.equals(httpException)) {
278+
ex.addSuppressed(httpException);
279+
}
280+
throw ex;
281+
}
282+
} catch (Exception e) {
283+
throw new MessageException("Failed to query server version: " + formatMessage(e), e);
284+
}
285+
}
286+
287+
private static String formatMessage(Exception e) {
288+
if (e instanceof HttpException) {
289+
String message = "HTTP " + ((HttpException) e).getCode();
290+
if (StringUtils.isNotBlank(e.getMessage())) {
291+
message += " " + e.getMessage();
277292
}
293+
return message;
278294
}
295+
return e.getMessage();
279296
}
280297

281298
private void initBootstrapDefaultValues() {

lib/src/main/java/org/sonarsource/scanner/lib/internal/http/HttpException.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.sonarsource.scanner.lib.internal.http;
2121

2222
import java.net.URL;
23+
import java.util.Objects;
2324
import javax.annotation.Nullable;
2425

2526
public class HttpException extends RuntimeException {
@@ -47,4 +48,16 @@ public int getCode() {
4748
public String getBody() {
4849
return body;
4950
}
51+
52+
@Override
53+
public boolean equals(Object o) {
54+
if (!(o instanceof HttpException)) return false;
55+
HttpException that = (HttpException) o;
56+
return code == that.code && Objects.equals(requestUrl, that.requestUrl) && Objects.equals(body, that.body) && Objects.equals(getMessage(), that.getMessage());
57+
}
58+
59+
@Override
60+
public int hashCode() {
61+
return Objects.hash(requestUrl, code, body, getMessage());
62+
}
5063
}

lib/src/test/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapperTest.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ void should_use_new_bootstrapping_with_sonarqube_10_6() throws Exception {
128128
}
129129
}
130130

131+
@Test
132+
void should_report_apiv2_error_with_sq_10_6_even_if_older_version_ws_succeeded() throws Exception {
133+
when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new HttpException(URI.create("http://myserver").toURL(), 401, "", null));
134+
when(scannerHttpClient.callWebApi("/api/server/version")).thenReturn(SQ_VERSION_NEW_BOOTSTRAPPING);
135+
try (var bootstrapResult = underTest.setBootstrapProperty(ScannerProperties.HOST_URL, "http://localhost").bootstrap()) {
136+
assertThat(bootstrapResult.isSuccessful()).isFalse();
137+
}
138+
139+
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: HTTP 401. Please check the property sonar.token or the environment variable SONAR_TOKEN.");
140+
}
141+
142+
@Test
143+
void should_report_technical_errors() throws Exception {
144+
when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new IOException("Socket closed"));
145+
try (var bootstrapResult = underTest.setBootstrapProperty(ScannerProperties.HOST_URL, "http://localhost").bootstrap()) {
146+
assertThat(bootstrapResult.isSuccessful()).isFalse();
147+
}
148+
149+
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: Socket closed");
150+
}
151+
131152
@Test
132153
void should_issue_deprecation_warning_for_sonar_login_property_sonarqube_10_0() throws Exception {
133154
IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class);
@@ -162,6 +183,25 @@ void should_log_cb_server_type() throws Exception {
162183
}
163184
}
164185

186+
@Test
187+
void should_use_old_bootstrapping_with_sonarqube_9_9() throws Exception {
188+
IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class);
189+
when(launcherFactory.createLauncher(eq(scannerHttpClient), any(FileCache.class)))
190+
.thenReturn(mock(IsolatedLauncherFactory.IsolatedLauncherAndClassloader.class));
191+
192+
ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, scannerHttpClient,
193+
launcherFactory, scannerEngineLauncherFactory);
194+
when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new HttpException(URI.create("http://myserver").toURL(), 401, "Unauthorized", null));
195+
when(scannerHttpClient.callWebApi("/api/server/version")).thenReturn("9.9");
196+
197+
try (var bootstrapResult = bootstrapper.setBootstrapProperty(ScannerProperties.HOST_URL, "http://myserver").bootstrap()) {
198+
verify(launcherFactory).createLauncher(eq(scannerHttpClient), any(FileCache.class));
199+
assertThat(bootstrapResult.getEngineFacade().isSonarCloud()).isFalse();
200+
assertThat(bootstrapResult.getEngineFacade().getServerVersion()).isEqualTo("9.9");
201+
verifySonarQubeServerTypeLogged("9.9");
202+
}
203+
}
204+
165205
@Test
166206
void should_use_old_bootstrapping_with_sonarqube_10_5() throws Exception {
167207
IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class);
@@ -190,14 +230,15 @@ void should_show_help_on_proxy_auth_error() throws Exception {
190230
ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, scannerHttpClient,
191231
launcherFactory, scannerEngineLauncherFactory);
192232
when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new HttpException(URI.create("http://myserver").toURL(), 407, "Proxy Authentication Required", null));
233+
when(scannerHttpClient.callWebApi("/api/server/version")).thenThrow(new HttpException(URI.create("http://myserver").toURL(), 407, "Proxy Authentication Required", null));
193234

194235
logTester.setLevel(Level.DEBUG);
195236

196237
try (var result = bootstrapper.setBootstrapProperty(ScannerProperties.HOST_URL, "http://myserver").bootstrap()) {
197238
assertThat(result.isSuccessful()).isFalse();
198239
}
199240

200-
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: Proxy Authentication Required. Please check the properties sonar.scanner.proxyUser and " +
241+
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: HTTP 407 Proxy Authentication Required. Please check the properties sonar.scanner.proxyUser and " +
201242
"sonar.scanner.proxyPassword.");
202243
assertThatNoServerTypeIsLogged();
203244
}
@@ -221,7 +262,7 @@ void should_preserve_both_exceptions_when_checking_version() throws Exception {
221262

222263
var loggedError = logTester.logEvents(Level.ERROR);
223264
assertThat(loggedError).hasSize(1);
224-
assertThat(loggedError.get(0).getFormattedMessage()).contains("Failed to query server version: Server Error");
265+
assertThat(loggedError.get(0).getFormattedMessage()).contains("Failed to query server version: HTTP 400 Server Error");
225266
assertThat(ThrowableProxyUtil.asString(loggedError.get(0).getThrowableProxy()))
226267
.containsSubsequence(
227268
"Suppressed: org.sonarsource.scanner.lib.internal.http.HttpException: Not Found",
@@ -246,7 +287,7 @@ void should_log_user_friendly_message_when_auth_error(int code) throws Exception
246287
assertThat(result.isSuccessful()).isFalse();
247288
}
248289

249-
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: Unauthorized. Please check the property sonar.token or the environment variable SONAR_TOKEN" +
290+
assertThat(logTester.logs(Level.ERROR)).contains("Failed to query server version: HTTP " + code + " Unauthorized. Please check the property sonar.token or the environment variable SONAR_TOKEN" +
250291
".");
251292
assertThatNoServerTypeIsLogged();
252293
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.scanner.lib.internal.http;
21+
22+
import java.net.MalformedURLException;
23+
import java.net.URI;
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
class HttpExceptionTest {
29+
30+
@Test
31+
void testEqualsAndHashCode() throws MalformedURLException {
32+
var underTest1 = new HttpException(URI.create("http://foo1").toURL(), 401, "message1", "body1");
33+
var sameUnderTest = new HttpException(URI.create("http://foo1").toURL(), 401, "message1", "body1");
34+
35+
assertThat(underTest1)
36+
.hasSameHashCodeAs(sameUnderTest)
37+
.isEqualTo(sameUnderTest)
38+
.isNotEqualTo(new HttpException(URI.create("http://foo2").toURL(), 401, "message1", "body1"))
39+
.isNotEqualTo(new HttpException(URI.create("http://foo1").toURL(), 402, "message1", "body1"))
40+
.isNotEqualTo(new HttpException(URI.create("http://foo1").toURL(), 401, "message2", "body1"))
41+
.isNotEqualTo(new HttpException(URI.create("http://foo1").toURL(), 401, "message1", "body2"));
42+
}
43+
}

0 commit comments

Comments
 (0)