aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/public/WEB-INF/app-content.xml17
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java5
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java5
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java9
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java11
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java43
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java2
-rw-r--r--server/sonar-webserver-webapi-v2/build.gradle22
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/LogComponent.java48
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java29
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java37
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java36
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java50
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java47
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java59
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java52
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/controller/DefautLivenessControllerTest.java100
-rw-r--r--server/sonar-webserver-webapi/build.gradle8
-rw-r--r--server/sonar-webserver/build.gradle1
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java20
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/ApiV2Servlet.java130
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java1
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java1
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java6
-rw-r--r--server/sonar-webserver/src/test/java/org/sonar/server/platform/web/ApiV2ServletTest.java194
-rw-r--r--settings.gradle1
-rw-r--r--sonar-application/build.gradle2
27 files changed, 902 insertions, 34 deletions
diff --git a/server/sonar-web/public/WEB-INF/app-content.xml b/server/sonar-web/public/WEB-INF/app-content.xml
new file mode 100644
index 00000000000..6593bb29d75
--- /dev/null
+++ b/server/sonar-web/public/WEB-INF/app-content.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:mvc="http://www.springframework.org/schema/mvc"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context
+ http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/mvc
+ http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
+
+ <context:annotation-config/>
+ <context:component-scan base-package="org.sonar.server.v2"/>
+ <mvc:annotation-driven />
+</beans>
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java
index e6fe4e594ed..8c26397300f 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java
@@ -19,7 +19,6 @@
*/
package org.sonar.server.authentication;
-import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Set;
import javax.servlet.FilterChain;
@@ -43,10 +42,10 @@ public class DefaultAdminCredentialsVerifierFilter extends ServletFilter {
// is why this is not defined in org.sonar.process.ProcessProperties.
private static final String SONAR_FORCE_REDIRECT_DEFAULT_ADMIN_CREDENTIALS = "sonar.forceRedirectOnDefaultAdminCredentials";
- private static final Set<String> SKIPPED_URLS = ImmutableSet.of(
+ private static final Set<String> SKIPPED_URLS = Set.of(
RESET_PASSWORD_PATH,
CHANGE_ADMIN_PASSWORD_PATH,
- "/batch/*", "/api/*");
+ "/batch/*", "/api/*", "/api/v2/*");
private final Configuration config;
private final DefaultAdminCredentialsVerifier defaultAdminCredentialsVerifier;
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java
index 7c19bf0354e..311c93d5b0e 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java
@@ -19,7 +19,6 @@
*/
package org.sonar.server.authentication;
-import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Set;
import javax.servlet.FilterChain;
@@ -38,9 +37,9 @@ import static org.sonar.server.authentication.AuthenticationRedirection.redirect
public class ResetPasswordFilter extends ServletFilter {
private static final String RESET_PASSWORD_PATH = "/account/reset_password";
- private static final Set<String> SKIPPED_URLS = ImmutableSet.of(
+ private static final Set<String> SKIPPED_URLS = Set.of(
RESET_PASSWORD_PATH,
- "/batch/*", "/api/*");
+ "/batch/*", "/api/*", "/api/v2/*");
private final ThreadLocalUserSession userSession;
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java
index eb4f4855dee..b01240a79c4 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.user;
+import javax.annotation.Nullable;
import org.sonar.api.server.ws.Request;
/**
@@ -33,8 +34,14 @@ public interface SystemPasscode {
/**
* Whether the system passcode is provided by the HTTP request or not.
- * Returns {@code false} if passcode is not configured.
+ * Returns {@code false} if passcode is not configured or not valid.
*/
boolean isValid(Request request);
+ /**
+ * Check if the passcode passed as argument is valid.
+ * Returns {@code false} if passcode is not configured or not valid.
+ */
+ boolean isValidPasscode(@Nullable String passcode);
+
}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java
index 047f8e2a8b2..b04bede3b3f 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java
@@ -19,7 +19,9 @@
*/
package org.sonar.server.user;
+import java.util.Objects;
import java.util.Optional;
+import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.Startable;
import org.sonar.api.config.Configuration;
@@ -46,8 +48,13 @@ public class SystemPasscodeImpl implements SystemPasscode, Startable {
if (configuredPasscode == null) {
return false;
}
- return request.header(PASSCODE_HTTP_HEADER)
- .map(s -> configuredPasscode.equals(s))
+ return isValidPasscode(request.header(PASSCODE_HTTP_HEADER).orElse(null));
+ }
+
+ @Override
+ public boolean isValidPasscode(@Nullable String passcode) {
+ return Optional.ofNullable(passcode)
+ .map(s -> Objects.equals(configuredPasscode, s))
.orElse(false);
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java
index f4d5bddbd1d..ee874c6692b 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java
@@ -19,9 +19,13 @@
*/
package org.sonar.server.user;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.impl.ws.SimpleGetRequest;
import org.sonar.api.utils.log.LogTester;
@@ -29,6 +33,7 @@ import org.sonar.api.utils.log.LoggerLevel;
import static org.assertj.core.api.Assertions.assertThat;
+@RunWith(DataProviderRunner.class)
public class SystemPasscodeImplTest {
@Rule
@@ -65,36 +70,34 @@ public class SystemPasscodeImplTest {
assertThat(logTester.logs(LoggerLevel.INFO)).contains("System authentication by passcode is disabled");
}
- @Test
- public void isValid_is_true_if_request_header_matches_configured_passcode() {
- verifyIsValid(true, "foo", "foo");
+ @DataProvider
+ public static Object[][] passcodeConfigurationAndUserInput() {
+ return new Object[][] {
+ {"toto", "toto", true},
+ {"toto", "tata", false},
+ {"toto", "Toto", false},
+ {"toto", "toTo", false},
+ {null, null, false},
+ {null, "toto", false},
+ {"toto", null, false},
+ };
}
- @Test
- public void isValid_is_false_if_request_header_matches_configured_passcode_with_different_case() {
- verifyIsValid(false, "foo", "FOO");
- }
@Test
- public void isValid_is_false_if_request_header_does_not_match_configured_passcode() {
- verifyIsValid(false, "foo", "bar");
- }
-
- @Test
- public void isValid_is_false_if_request_header_is_defined_but_passcode_is_not_configured() {
- verifyIsValid(false, null, "foo");
+ @UseDataProvider("passcodeConfigurationAndUserInput")
+ public void isValidPasscode_worksCorrectly(String configuredPasscode, String userPasscode, boolean expectedResult) {
+ configurePasscode(configuredPasscode);
+ assertThat(underTest.isValidPasscode(userPasscode)).isEqualTo(expectedResult);
}
@Test
- public void isValid_is_false_if_request_header_is_empty() {
- verifyIsValid(false, "foo", "");
- }
-
- private void verifyIsValid(boolean expectedResult, String configuredPasscode, String header) {
+ @UseDataProvider("passcodeConfigurationAndUserInput")
+ public void isValid_worksCorrectly(String configuredPasscode, String userPasscode, boolean expectedResult) {
configurePasscode(configuredPasscode);
SimpleGetRequest request = new SimpleGetRequest();
- request.setHeader("X-Sonar-Passcode", header);
+ request.setHeader("X-Sonar-Passcode", userPasscode);
assertThat(underTest.isValid(request)).isEqualTo(expectedResult);
}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java
index 7b81cea09f7..355f060d6c6 100644
--- a/server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java
@@ -47,7 +47,7 @@ public class PluginsRiskConsentFilter extends ServletFilter {
PLUGINS_RISK_CONSENT_PATH,
"/account/reset_password",
"/admin/change_admin_password",
- "/batch/*", "/api/*");
+ "/batch/*", "/api/*", "/api/v2/*");
private final ThreadLocalUserSession userSession;
private final Configuration config;
diff --git a/server/sonar-webserver-webapi-v2/build.gradle b/server/sonar-webserver-webapi-v2/build.gradle
new file mode 100644
index 00000000000..0c41cadea53
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/build.gradle
@@ -0,0 +1,22 @@
+sonarqube {
+ properties {
+ property 'sonar.projectName', "${projectTitle} :: WebServer :: WebAPIV2"
+ }
+}
+
+dependencies {
+ // please keep the list grouped by configuration and ordered by name
+ api 'org.springframework:spring-webmvc:5.3.23'
+
+ api project(':server:sonar-db-dao')
+ // We are not suppose to have a v1 dependency. The ideal would be to have another common module between webapi and webapi-v2 but that needs a lot of refactoring.
+ api project(':server:sonar-webserver-webapi')
+
+ testImplementation 'org.mockito:mockito-core'
+ testImplementation 'org.springframework:spring-test:5.3.23'
+
+ testImplementation testFixtures(project(':server:sonar-server-common'))
+
+ testImplementation project(':sonar-testing-harness')
+}
+
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/LogComponent.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/LogComponent.java
new file mode 100644
index 00000000000..0fdaf56214c
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/LogComponent.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+@Component
+public class LogComponent implements ApplicationListener<ContextRefreshedEvent> {
+
+ private static final Logger LOGGER = Loggers.get(LogComponent.class);
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ ApplicationContext applicationContext = event.getApplicationContext();
+ List<Integer> isUseless = applicationContext.getBeansOfType(RequestMappingHandlerMapping.class).values().stream()
+ .map(AbstractHandlerMethodMapping::getHandlerMethods)
+ .map(d->{
+ d.forEach((e, c)-> LOGGER.info("Registered endpoint: "+e.getName()+" "+e.getDirectPaths()+" "+e));
+ return 1;
+ })
+ .collect(Collectors.toList());
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
new file mode 100644
index 00000000000..dd6fd559af0
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2;
+
+public class WebApiEndpoints {
+
+ public static final String LIVENESS_ENDPOINT = "/system/liveness";
+
+ private WebApiEndpoints() {
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
new file mode 100644
index 00000000000..170914c5ec2
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.common;
+
+import java.util.Optional;
+import org.sonar.server.exceptions.ServerException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class RestResponseEntityExceptionHandler {
+
+ @ExceptionHandler(ServerException.class)
+ protected ResponseEntity<Object> handleServerException(ServerException serverException) {
+ return new ResponseEntity<>(serverException.getMessage(), Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
new file mode 100644
index 00000000000..23a1fdc678a
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.config;
+
+import org.sonar.server.v2.common.RestResponseEntityExceptionHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+@Configuration
+@EnableWebMvc
+public class CommonWebConfig {
+
+ @Bean
+ public RestResponseEntityExceptionHandler restResponseEntityExceptionHandler() {
+ return new RestResponseEntityExceptionHandler();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
new file mode 100644
index 00000000000..45153c377cc
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.config;
+
+import org.sonar.server.health.CeStatusNodeCheck;
+import org.sonar.server.health.DbConnectionNodeCheck;
+import org.sonar.server.health.EsStatusNodeCheck;
+import org.sonar.server.health.WebServerStatusNodeCheck;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.platform.ws.LivenessCheckerImpl;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.controller.DefautLivenessController;
+import org.sonar.server.v2.controller.LivenessController;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@Import(CommonWebConfig.class)
+public class PlatformLevel4WebConfig {
+
+ @Bean
+ public LivenessChecker livenessChecker(DbConnectionNodeCheck dbConnectionNodeCheck, WebServerStatusNodeCheck webServerStatusNodeCheck, CeStatusNodeCheck ceStatusNodeCheck,
+ EsStatusNodeCheck esStatusNodeCheck) {
+ return new LivenessCheckerImpl(dbConnectionNodeCheck, webServerStatusNodeCheck, ceStatusNodeCheck, esStatusNodeCheck);
+ }
+
+ @Bean
+ public LivenessController livenessController(LivenessChecker livenessChecker, UserSession userSession, SystemPasscode systemPasscode) {
+ return new DefautLivenessController(livenessChecker, systemPasscode, userSession);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
new file mode 100644
index 00000000000..9fb7f717c6a
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.config;
+
+import org.sonar.server.health.DbConnectionNodeCheck;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.platform.ws.SafeModeLivenessCheckerImpl;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.controller.DefautLivenessController;
+import org.sonar.server.v2.controller.LivenessController;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+@Configuration
+@EnableWebMvc
+@Import(CommonWebConfig.class)
+public class SafeModeWebConfig {
+
+ @Bean
+ public LivenessChecker livenessChecker(DbConnectionNodeCheck dbConnectionNodeCheck) {
+ return new SafeModeLivenessCheckerImpl(dbConnectionNodeCheck);
+ }
+
+ @Bean
+ public LivenessController livenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode) {
+ return new DefautLivenessController(livenessChecker, systemPasscode, null);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java
new file mode 100644
index 00000000000..8641f7f8811
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.controller;
+
+import javax.annotation.Nullable;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class DefautLivenessController implements LivenessController {
+
+ private final LivenessChecker livenessChecker;
+ private final UserSession userSession;
+ private final SystemPasscode systemPasscode;
+
+ public DefautLivenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode, @Nullable UserSession userSession) {
+ this.livenessChecker = livenessChecker;
+ this.userSession = userSession;
+ this.systemPasscode = systemPasscode;
+ }
+
+ @Override
+ public void livenessCheck(String requestPassCode) {
+ if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
+ if (livenessChecker.liveness()) {
+ return;
+ }
+ throw new IllegalStateException("Liveness check failed");
+ }
+ throw new ForbiddenException("Insufficient privileges");
+ }
+
+ private boolean isSystemAdmin() {
+ if (userSession == null) {
+ return false;
+ }
+ return userSession.isSystemAdministrator();
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java
new file mode 100644
index 00000000000..3b4daf00620
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
+
+@RequestMapping(LIVENESS_ENDPOINT)
+public interface LivenessController {
+
+ @GetMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(summary = "Provide liveness of SonarQube, meant to be used as a liveness probe on Kubernetes", description = """
+ Require 'Administer System' permission or authentication with passcode.
+
+ When SonarQube is fully started, liveness check for database connectivity, Compute Engine status, and, except for DataCenter Edition, if ElasticSearch is Green or Yellow.
+
+ When SonarQube is on Safe Mode (for example when a database migration is running), liveness check only for database connectivity
+ """)
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "This SonarQube node is alive"),
+ @ApiResponse(description = "This SonarQube node is not alive and should be rescheduled"),
+ })
+ void livenessCheck(
+ @Parameter(description = "Passcode can be provided, see SonarQube documentation") @RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode);
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/controller/DefautLivenessControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/controller/DefautLivenessControllerTest.java
new file mode 100644
index 00000000000..c7c4762466b
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/controller/DefautLivenessControllerTest.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.controller;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.common.RestResponseEntityExceptionHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.when;
+import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DefautLivenessControllerTest {
+
+ private static final String PASSCODE = "1234";
+ @Mock
+ private LivenessChecker livenessChecker;
+ @Mock
+ private UserSession userSession;
+ @Mock
+ private SystemPasscode systemPasscode;
+ @InjectMocks
+ private DefautLivenessController defautLivenessController;
+
+ MockMvc mockMvc;
+
+ @Before
+ public void setUp() {
+ this.mockMvc = MockMvcBuilders.standaloneSetup(defautLivenessController)
+ .setControllerAdvice(RestResponseEntityExceptionHandler.class)
+ .build();
+ }
+
+ @Test
+ public void livenessCheck_should_returnForbiddenWithNoCredentials() throws Exception {
+ mockMvc.perform(get(LIVENESS_ENDPOINT))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ public void livenessCheck_should_returnForbiddenWithWrongPasscodeAndNoAdminCredentials() throws Exception {
+ when(systemPasscode.isValidPasscode(PASSCODE)).thenReturn(false);
+ when(userSession.isSystemAdministrator()).thenReturn(false);
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header("X-Sonar-Passcode", PASSCODE))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ public void livenessCheck_should_returnNoContentWithSystemPasscode() throws Exception {
+ when(systemPasscode.isValidPasscode(PASSCODE)).thenReturn(true);
+ when(livenessChecker.liveness()).thenReturn(true);
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header("X-Sonar-Passcode", PASSCODE))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ public void livenessCheck_should_returnNoContentWithWhenUserIsAdmin() throws Exception {
+ when(userSession.isSystemAdministrator()).thenReturn(true);
+ when(livenessChecker.liveness()).thenReturn(true);
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header("X-Sonar-Passcode", PASSCODE))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ public void livenessCheck_should_returnServerErrorWhenLivenessCheckFails() throws Exception {
+ when(systemPasscode.isValidPasscode(PASSCODE)).thenReturn(true);
+ when(livenessChecker.liveness()).thenReturn(false);
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header("X-Sonar-Passcode", PASSCODE))
+ .andExpect(status().isInternalServerError());
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/build.gradle b/server/sonar-webserver-webapi/build.gradle
index b8ee1eebe89..b891af08f70 100644
--- a/server/sonar-webserver-webapi/build.gradle
+++ b/server/sonar-webserver-webapi/build.gradle
@@ -13,6 +13,12 @@ dependencies {
api 'io.prometheus:simpleclient_common'
api 'io.prometheus:simpleclient_servlet'
+ api 'org.springframework:spring-webmvc:5.3.23'
+ api 'org.springframework:spring-web:5.3.23'
+ api 'org.springframework:spring-context:5.3.23'
+ api 'org.springframework:spring-core:5.3.23'
+ testImplementation 'org.springframework:spring-test:5.3.23'
+
api project(':server:sonar-ce-common')
api project(':server:sonar-ce-task')
api project(':server:sonar-db-dao')
@@ -38,6 +44,7 @@ dependencies {
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'javax.servlet:javax.servlet-api'
testImplementation 'org.mockito:mockito-core'
+ testImplementation 'org.springframework:spring-test:5.3.23'
testImplementation testFixtures(project(':server:sonar-server-common'))
testImplementation testFixtures(project(':server:sonar-webserver-auth'))
testImplementation testFixtures(project(':server:sonar-webserver-es'))
@@ -45,4 +52,3 @@ dependencies {
testImplementation project(':sonar-testing-harness')
testFixturesApi testFixtures(project(':server:sonar-db-dao'))
}
-
diff --git a/server/sonar-webserver/build.gradle b/server/sonar-webserver/build.gradle
index 0c24dec1822..f5cb9fe1c87 100644
--- a/server/sonar-webserver/build.gradle
+++ b/server/sonar-webserver/build.gradle
@@ -21,6 +21,7 @@ dependencies {
api project(':server:sonar-process')
api project(':server:sonar-webserver-core')
api project(':server:sonar-webserver-webapi')
+ api project(':server:sonar-webserver-webapi-v2')
api project(':server:sonar-webserver-pushapi')
api project(':server:sonar-webserver-monitoring')
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java
index 15755a9866e..a89de3719d8 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java
@@ -25,11 +25,13 @@ import java.util.List;
import java.util.Properties;
import javax.annotation.Nullable;
import javax.servlet.ServletContext;
+import javax.servlet.ServletRegistration;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
import org.sonar.core.platform.ExtensionContainer;
import org.sonar.core.platform.SpringComponentContainer;
+import org.sonar.server.platform.web.ApiV2Servlet;
import org.sonar.server.app.ProcessCommandWrapper;
import org.sonar.server.platform.db.migration.version.DatabaseVersion;
import org.sonar.server.platform.platformlevel.PlatformLevel;
@@ -39,6 +41,7 @@ import org.sonar.server.platform.platformlevel.PlatformLevel3;
import org.sonar.server.platform.platformlevel.PlatformLevel4;
import org.sonar.server.platform.platformlevel.PlatformLevelSafeMode;
import org.sonar.server.platform.platformlevel.PlatformLevelStartup;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.sonar.process.ProcessId.WEB_SERVER;
@@ -60,10 +63,12 @@ public class PlatformImpl implements Platform {
private PlatformLevel level3 = null;
private PlatformLevel level4 = null;
private PlatformLevel currentLevel = null;
+ private AnnotationConfigWebApplicationContext springMvcContext = null;
private boolean dbConnected = false;
private boolean started = false;
private final List<Object> level4AddedComponents = new ArrayList<>();
private final Profiler profiler = Profiler.createIfTrace(Loggers.get(PlatformImpl.class));
+ private ApiV2Servlet servlet;
public static PlatformImpl getInstance() {
return INSTANCE;
@@ -89,6 +94,10 @@ public class PlatformImpl implements Platform {
boolean dbRequiredMigration = dbRequiresMigration();
startSafeModeContainer();
currentLevel = levelSafeMode;
+ if (!started) {
+ registerSpringMvcServlet();
+ this.servlet.initDispatcherSafeMode(levelSafeMode);
+ }
started = true;
// if AutoDbMigration kicked in or no DB migration was required, startup can be resumed in another thread
@@ -96,6 +105,7 @@ public class PlatformImpl implements Platform {
LOGGER.info("Database needs to be migrated. Please refer to https://docs.sonarqube.org/latest/setup/upgrading");
} else {
this.autoStarter = createAutoStarter();
+
this.autoStarter.execute(new AutoStarterRunnable(autoStarter) {
@Override
public void doRun() {
@@ -104,7 +114,9 @@ public class PlatformImpl implements Platform {
}
runIfNotAborted(PlatformImpl.this::startLevel34Containers);
+ runIfNotAborted(()->servlet.initDispatcherLevel4(level4));
runIfNotAborted(PlatformImpl.this::executeStartupTasks);
+
// switch current container last to avoid giving access to a partially initialized container
runIfNotAborted(() -> {
currentLevel = level4;
@@ -118,6 +130,13 @@ public class PlatformImpl implements Platform {
}
}
+ private void registerSpringMvcServlet() {
+ servlet = new ApiV2Servlet();
+ ServletRegistration.Dynamic app = this.servletContext.addServlet("app", servlet);
+ app.addMapping("/api/v2/*");
+ app.setLoadOnStartup(1);
+ }
+
private AutoStarter createAutoStarter() {
ProcessCommandWrapper processCommandWrapper = getContainer().getComponentByType(ProcessCommandWrapper.class);
return new AsynchronousAutoStarter(processCommandWrapper);
@@ -177,6 +196,7 @@ public class PlatformImpl implements Platform {
level4 = start(new PlatformLevel4(level3, level4AddedComponents));
}
+
private void executeStartupTasks() {
new PlatformLevelStartup(level4)
.configure()
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/ApiV2Servlet.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/ApiV2Servlet.java
new file mode 100644
index 00000000000..9603c2d70a8
--- /dev/null
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/ApiV2Servlet.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.web;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.util.function.Function;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.server.platform.platformlevel.PlatformLevel;
+import org.sonar.server.v2.config.PlatformLevel4WebConfig;
+import org.sonar.server.v2.config.SafeModeWebConfig;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+
+public class ApiV2Servlet implements Servlet {
+ public static final String SERVLET_NAME = "WebAPI V2 Servlet";
+ private Function<WebApplicationContext, DispatcherServlet> servletProvider;
+ private ServletConfig config = null;
+ private DispatcherServlet dispatcherLevel4 = null;
+ private DispatcherServlet dispatcherSafeMode = null;
+
+ public ApiV2Servlet() {
+ this.servletProvider = DispatcherServlet::new;
+ }
+
+ @VisibleForTesting
+ void setServletProvider(Function<WebApplicationContext, DispatcherServlet> servletProvider) {
+ this.servletProvider = servletProvider;
+ }
+
+ @Override
+ public String getServletInfo() {
+ return SERVLET_NAME;
+ }
+
+ @Override
+ public ServletConfig getServletConfig() {
+ return config;
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ this.config = config;
+ if (dispatcherLevel4 != null) {
+ dispatcherLevel4.init(config);
+ }
+ if (dispatcherSafeMode != null) {
+ dispatcherSafeMode.init(config);
+ }
+ }
+
+ public void initDispatcherSafeMode(PlatformLevel platformLevel) {
+ this.dispatcherSafeMode = initDispatcherServlet(platformLevel, SafeModeWebConfig.class);
+ }
+
+ public void initDispatcherLevel4(PlatformLevel platformLevel) {
+ dispatcherLevel4 = initDispatcherServlet(platformLevel, PlatformLevel4WebConfig.class);
+ destroyDispatcherSafeMode();
+ }
+
+ private DispatcherServlet initDispatcherServlet(PlatformLevel platformLevel, Class<?> configClass) {
+ AnnotationConfigWebApplicationContext springMvcContext = new AnnotationConfigWebApplicationContext();
+ springMvcContext.setParent(platformLevel.getContainer().context());
+ springMvcContext.register(configClass);
+ DispatcherServlet dispatcher = servletProvider.apply(springMvcContext);
+ try {
+ if (config != null) {
+ dispatcher.init(config);
+ }
+ } catch (ServletException e) {
+ throw new RuntimeException(e);
+ }
+ return dispatcher;
+ }
+
+ @Override
+ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
+ if (dispatcherSafeMode != null) {
+ dispatcherSafeMode.service(req, res);
+ } else if (dispatcherLevel4 != null) {
+ dispatcherLevel4.service(req, res);
+ } else {
+ HttpServletResponse httpResponse = (HttpServletResponse) res;
+ httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ destroyDispatcherSafeMode();
+ destroyLevel4();
+ }
+
+ private void destroyDispatcherSafeMode() {
+ if (dispatcherSafeMode != null) {
+ DispatcherServlet dispatcherToDestroy = dispatcherSafeMode;
+ dispatcherSafeMode = null;
+ dispatcherToDestroy.destroy();
+ }
+ }
+
+ private void destroyLevel4() {
+ if (dispatcherLevel4 != null) {
+ dispatcherLevel4.destroy();
+ }
+ }
+}
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java
index 17e01a263ad..e5ba6d2aca9 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java
@@ -38,6 +38,7 @@ import static java.util.concurrent.TimeUnit.HOURS;
public class SonarLintConnectionFilter extends ServletFilter {
private static final UrlPattern URL_PATTERN = UrlPattern.builder()
.includes("/api/*")
+ .excludes("/api/v2/*")
.build();
private final DbClient dbClient;
private final ThreadLocalUserSession userSession;
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
index 54ea6cacd73..5e32a354f14 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
@@ -51,6 +51,7 @@ public class WebPagesFilter implements Filter {
private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
.builder()
.excludes(staticResourcePatterns())
+ .excludes("/api/v2/*")
.build();
private WebPagesCache webPagesCache;
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java
index 4660820851d..5d04f9e0381 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.platform.web;
+import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -34,6 +35,7 @@ import org.sonar.server.ws.ServletRequest;
import org.sonar.server.ws.ServletResponse;
import org.sonar.server.ws.WebServiceEngine;
+import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Stream.concat;
import static org.sonar.server.platform.web.WebServiceReroutingFilter.MOVED_WEB_SERVICES;
@@ -63,8 +65,8 @@ public class WebServiceFilter extends ServletFilter {
webServiceEngine.controllers().stream()
.flatMap(controller -> controller.actions().stream())
.filter(action -> action.handler() instanceof ServletFilterHandler)
- .map(toPath()))
- .collect(MoreCollectors.toSet());
+ .map(toPath())).collect(toCollection(HashSet::new));
+ excludeUrls.add("/api/v2/*");
}
@Override
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/ApiV2ServletTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/ApiV2ServletTest.java
new file mode 100644
index 00000000000..14f64c3599c
--- /dev/null
+++ b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/ApiV2ServletTest.java
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.web;
+
+import java.io.IOException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+import org.sonar.core.platform.SpringComponentContainer;
+import org.sonar.server.platform.platformlevel.PlatformLevel;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ApiV2ServletTest {
+
+ @Test
+ public void getServletConfig_shouldReturnServletConfig() throws ServletException {
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ ServletConfig mockServletConfig = mock(ServletConfig.class);
+ underTest.init(mockServletConfig);
+
+ assertThat(underTest.getServletConfig()).isEqualTo(mockServletConfig);
+ }
+
+ @Test
+ public void getServletInfo_shouldReturnOwnDefinedServletName() {
+ ApiV2Servlet underTest = new ApiV2Servlet();
+
+ assertThat(underTest.getServletInfo())
+ .isEqualTo(ApiV2Servlet.SERVLET_NAME);
+ }
+
+ @Test
+ public void init_shouldInitDispatcherSafeModeConfig_whenDispatcherHadBeenInitWithoutConfig() throws ServletException {
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ DispatcherServlet mockDispatcherServletSafeMode = mock(DispatcherServlet.class);
+ underTest.setServletProvider(context -> mockDispatcherServletSafeMode);
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ underTest.initDispatcherSafeMode(mockPlatformLevel);
+
+ ServletConfig mockServletConfig = mock(ServletConfig.class);
+ underTest.init(mockServletConfig);
+
+ verify(mockDispatcherServletSafeMode, times(1)).init(mockServletConfig);
+ }
+
+ @Test
+ public void init_shouldInitDispatcherLevel4Config_whenDispatcherHadBeenInitWithoutConfig() throws ServletException {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+
+ DispatcherServlet mockDispatcherServletLevel4 = mock(DispatcherServlet.class);
+ underTest.setServletProvider(context -> mockDispatcherServletLevel4);
+ underTest.initDispatcherLevel4(mockPlatformLevel);
+
+ ServletConfig mockServletConfig = mock(ServletConfig.class);
+
+ underTest.init(mockServletConfig);
+
+ verify(mockDispatcherServletLevel4, times(1)).init(mockServletConfig);
+ }
+
+ @Test
+ public void service_shouldDispatchOnRightDispatcher() throws ServletException, IOException {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ DispatcherServlet mockDispatcherServletSafeMode = mock(DispatcherServlet.class);
+ DispatcherServlet mockDispatcherServletLevel4 = mock(DispatcherServlet.class);
+
+ underTest.setServletProvider(context -> mockDispatcherServletSafeMode);
+ underTest.init(mock(ServletConfig.class));
+ underTest.initDispatcherSafeMode(mockPlatformLevel);
+ ServletRequest mockRequest1 = mock(ServletRequest.class);
+ ServletResponse mockResponse1 = mock(ServletResponse.class);
+ underTest.service(mockRequest1, mockResponse1);
+ verify(mockDispatcherServletSafeMode, times(1)).service(mockRequest1, mockResponse1);
+
+ underTest.setServletProvider(context -> mockDispatcherServletLevel4);
+ underTest.initDispatcherLevel4(mockPlatformLevel);
+ ServletRequest mockRequest2 = mock(ServletRequest.class);
+ ServletResponse mockResponse2 = mock(ServletResponse.class);
+ underTest.service(mockRequest2, mockResponse2);
+ verify(mockDispatcherServletLevel4, times(1)).service(mockRequest2, mockResponse2);
+ }
+
+ @Test
+ public void service_shouldReturnNotFound_whenDispatchersAreNotAvailable() throws ServletException, IOException {
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ HttpServletResponse mockResponse = mock(HttpServletResponse.class);
+
+ underTest.service(mock(ServletRequest.class), mockResponse);
+
+ verify(mockResponse, times(1)).sendError(SC_NOT_FOUND);
+ }
+
+ @Test
+ public void initDispatcherServlet_shouldThrowRuntimeException_whenDispatcherInitFails() throws ServletException {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ ServletConfig mockServletConfig = mock(ServletConfig.class);
+ String exceptionMessage = "Exception message";
+
+ DispatcherServlet mockDispatcherServletSafeMode = mock(DispatcherServlet.class);
+ doThrow(new ServletException(exceptionMessage)).when(mockDispatcherServletSafeMode).init(mockServletConfig);
+
+ underTest.setServletProvider(context -> mockDispatcherServletSafeMode);
+ underTest.init(mockServletConfig);
+
+ assertThatThrownBy(() -> underTest.initDispatcherSafeMode(mockPlatformLevel))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining(exceptionMessage);
+ }
+
+ @Test
+ public void initDispatcherServlet_initLevel4ShouldDestroySafeMode() {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+
+ DispatcherServlet mockDispatcherServletSafeMode = mock(DispatcherServlet.class);
+ underTest.setServletProvider(context -> mockDispatcherServletSafeMode);
+ underTest.initDispatcherSafeMode(mockPlatformLevel);
+
+ underTest.setServletProvider(context -> mock(DispatcherServlet.class));
+
+ underTest.initDispatcherLevel4(mockPlatformLevel);
+
+ verify(mockDispatcherServletSafeMode, times(1)).destroy();
+ }
+
+ @Test
+ public void destroy_shouldDestroyDispatcherLevel4() {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ DispatcherServlet mockDispatcherServletLevel4 = mock(DispatcherServlet.class);
+
+ underTest.setServletProvider(context -> mockDispatcherServletLevel4);
+ underTest.initDispatcherLevel4(mockPlatformLevel);
+
+ underTest.destroy();
+
+ verify(mockDispatcherServletLevel4, times(1)).destroy();
+ }
+
+ @Test
+ public void destroy_shouldDestroyDispatcherSafeMode() {
+ PlatformLevel mockPlatformLevel = getMockPlatformLevel();
+ ApiV2Servlet underTest = new ApiV2Servlet();
+ DispatcherServlet mockDispatcherServletSafeMode = mock(DispatcherServlet.class);
+
+ underTest.setServletProvider(context -> mockDispatcherServletSafeMode);
+ underTest.initDispatcherSafeMode(mockPlatformLevel);
+
+ underTest.destroy();
+
+ verify(mockDispatcherServletSafeMode, times(1)).destroy();
+ }
+
+ private static PlatformLevel getMockPlatformLevel() {
+ SpringComponentContainer mockSpringComponentContainer = mock(SpringComponentContainer.class);
+ when(mockSpringComponentContainer.context()).thenReturn(new AnnotationConfigApplicationContext());
+ PlatformLevel mockPlatformLevel = mock(PlatformLevel.class);
+ when(mockPlatformLevel.getContainer()).thenReturn(mockSpringComponentContainer);
+ return mockPlatformLevel;
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 5421562cd92..5732248f08b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -38,6 +38,7 @@ include 'server:sonar-webserver-auth'
include 'server:sonar-webserver-core'
include 'server:sonar-webserver-es'
include 'server:sonar-webserver-webapi'
+include 'server:sonar-webserver-webapi-v2'
include 'server:sonar-webserver-pushapi'
include 'server:sonar-webserver-ws'
include 'server:sonar-alm-client'
diff --git a/sonar-application/build.gradle b/sonar-application/build.gradle
index 54e2ac87de7..94defd62f61 100644
--- a/sonar-application/build.gradle
+++ b/sonar-application/build.gradle
@@ -43,7 +43,7 @@ jar.enabled = false
shadowJar {
archiveBaseName = 'sonar-application'
archiveClassifier = null
- mergeServiceFiles()
+ mergeServiceFiles('META-INF/spring.*')
manifest {
attributes('Main-Class': 'org.sonar.application.App')
}