]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18484 Add api/v2/system/liveness endpoint
authorPierre <pierre.guillot@sonarsource.com>
Wed, 27 Jul 2022 08:32:33 +0000 (10:32 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 21 Feb 2023 12:02:56 +0000 (12:02 +0000)
27 files changed:
server/sonar-web/public/WEB-INF/app-content.xml [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscode.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java
server/sonar-webserver-core/src/main/java/org/sonar/server/plugins/PluginsRiskConsentFilter.java
server/sonar-webserver-webapi-v2/build.gradle [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/LogComponent.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/controller/DefautLivenessControllerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/build.gradle
server/sonar-webserver/build.gradle
server/sonar-webserver/src/main/java/org/sonar/server/platform/PlatformImpl.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/ApiV2Servlet.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java
server/sonar-webserver/src/test/java/org/sonar/server/platform/web/ApiV2ServletTest.java [new file with mode: 0644]
settings.gradle
sonar-application/build.gradle

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 (file)
index 0000000..6593bb2
--- /dev/null
@@ -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>
index e6fe4e594ed3caeedf24bd8b0120094a9a2ef48d..8c26397300fe20b670cbe405903ca06b55b96e6a 100644 (file)
@@ -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;
index 7c19bf0354eba4b45983093a97e6c8690a15f983..311c93d5b0e4701163d5ca5a043b0e687bdb3a55 100644 (file)
@@ -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;
 
index eb4f4855deedf47cedd85ff093b4d49a6f4189c9..b01240a79c457c92acefc7a9ffd829f2fcf789e9 100644 (file)
@@ -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);
+
 }
index 047f8e2a8b2bf5492085c0c19c639adc9a19d7e7..b04bede3b3f0c58ee48de73145bb3d59ea881ee2 100644 (file)
@@ -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);
   }
 
index f4d5bddbd1d260221573ad2f23eaf4fe0287c510..ee874c6692b75ffe3511ec5e23bfc61650d41a06 100644 (file)
  */
 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);
   }
index 7b81cea09f792af65dd3d06c7c7e74b33a3ac7c6..355f060d6c6cc407e6d760eed130178b3e2866bc 100644 (file)
@@ -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 (file)
index 0000000..0c41cad
--- /dev/null
@@ -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 (file)
index 0000000..0fdaf56
--- /dev/null
@@ -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 (file)
index 0000000..dd6fd55
--- /dev/null
@@ -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 (file)
index 0000000..170914c
--- /dev/null
@@ -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 (file)
index 0000000..23a1fdc
--- /dev/null
@@ -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 (file)
index 0000000..45153c3
--- /dev/null
@@ -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 (file)
index 0000000..9fb7f71
--- /dev/null
@@ -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 (file)
index 0000000..8641f7f
--- /dev/null
@@ -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 (file)
index 0000000..3b4daf0
--- /dev/null
@@ -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 (file)
index 0000000..c7c4762
--- /dev/null
@@ -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());
+  }
+
+}
index b8ee1eebe89414ed0090595444005b92b0766928..b891af08f70a58ab46c088f26b5f4d662d8dd18f 100644 (file)
@@ -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'))
 }
-
index 0c24dec1822979f801669a5658700ea7c163426c..f5cb9fe1c879a107c801f72a379553da7ccea3bf 100644 (file)
@@ -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')
 
index 15755a9866e962ca24b5ce89ddf6cd74f45bbcee..a89de3719d8c47bec3dd3de72acf169b4230593f 100644 (file)
@@ -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 (file)
index 0000000..9603c2d
--- /dev/null
@@ -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();
+    }
+  }
+}
index 17e01a263adb113c95437e8fb91f8952cd759d2c..e5ba6d2aca933280a7e555b40d176bc5546113c1 100644 (file)
@@ -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;
index 54ea6cacd733c85da68985a0a5108a7b509fa740..5e32a354f146149ebe19ef1184b8fafe9ef0c2dc 100644 (file)
@@ -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;
index 4660820851d5e265ca988a91e4ae3ef86ffd3a1b..5d04f9e0381d9ec696b4ec792f966ab8bc9ca4a6 100644 (file)
@@ -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 (file)
index 0000000..14f64c3
--- /dev/null
@@ -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;
+  }
+}
index 5421562cd924f4b53225a14c77b33a9541f1fc9a..5732248f08b78a904a34589e5e2226bb465d5bec 100644 (file)
@@ -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'
index 54e2ac87de79bc3d477b5fd6af6c8f9404efaf0c..94defd62f61d86197621a4fb1c69ed4dbd99ff57 100644 (file)
@@ -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')
   }