--- /dev/null
+<?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>
*/
package org.sonar.server.authentication;
-import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Set;
import javax.servlet.FilterChain;
// 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;
*/
package org.sonar.server.authentication;
-import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Set;
import javax.servlet.FilterChain;
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;
*/
package org.sonar.server.user;
+import javax.annotation.Nullable;
import org.sonar.api.server.ws.Request;
/**
/**
* 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);
+
}
*/
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;
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);
}
*/
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;
import static org.assertj.core.api.Assertions.assertThat;
+@RunWith(DataProviderRunner.class)
public class SystemPasscodeImplTest {
@Rule
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);
}
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;
--- /dev/null
+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')
+}
+
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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() {
+ }
+
+}
--- /dev/null
+/*
+ * 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}
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')
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'))
testImplementation project(':sonar-testing-harness')
testFixturesApi testFixtures(project(':server:sonar-db-dao'))
}
-
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')
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;
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;
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;
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
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() {
}
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;
}
}
+ 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);
level4 = start(new PlatformLevel4(level3, level4AddedComponents));
}
+
private void executeStartupTasks() {
new PlatformLevelStartup(level4)
.configure()
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
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;
private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
.builder()
.excludes(staticResourcePatterns())
+ .excludes("/api/v2/*")
.build();
private WebPagesCache webPagesCache;
*/
package org.sonar.server.platform.web;
+import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
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;
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
--- /dev/null
+/*
+ * 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;
+ }
+}
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'
shadowJar {
archiveBaseName = 'sonar-application'
archiveClassifier = null
- mergeServiceFiles()
+ mergeServiceFiles('META-INF/spring.*')
manifest {
attributes('Main-Class': 'org.sonar.application.App')
}