testImplementation 'org.mockito:mockito-core'
testImplementation 'org.springframework:spring-test'
testImplementation 'org.skyscreamer:jsonassert:1.5.1'
+ testImplementation project(':sonar-testing-harness')
testImplementation testFixtures(project(':server:sonar-server-common'))
-
- testImplementation project(':sonar-testing-harness')
+ testImplementation testFixtures(project(':server:sonar-webserver-auth'))
}
+++ /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.api.user.controller;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.db.DbClient;
-import org.sonar.db.user.UserDao;
-import org.sonar.server.common.user.service.UserService;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.user.AbstractUserSession;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.v2.common.ControllerIT;
-
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-public class DefaultUserControllerIT extends ControllerIT {
-
- @After
- public void resetUsedMocks() {
- Mockito.reset(webAppContext.getBean(UserService.class));
- Mockito.reset(webAppContext.getBean(UserSession.class));
- }
-
- @Before
- public void setUp() {
- UserSession userSession = webAppContext.getBean(UserSession.class);
- when(userSession.checkLoggedIn()).thenReturn(userSession);
- }
-
- @Test
- public void deactivate_whenUserIsNotLoggedIn_shouldReturnForbidden() throws Exception {
- when(webAppContext.getBean(UserSession.class).checkLoggedIn()).thenThrow(new UnauthorizedException("unauthorized"));
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpectAll(
- status().isUnauthorized(),
- content().string("{\"message\":\"unauthorized\"}"));
- }
-
- @Test
- public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
- when(webAppContext.getBean(UserSession.class).checkIsSystemAdministrator()).thenThrow(AbstractUserSession.insufficientPrivilegesException());
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpectAll(
- status().isForbidden(),
- content().json("{\"message\":\"Insufficient privileges\"}"));
- }
-
- @Test
- public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
- doThrow(new NotFoundException("User not found.")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpectAll(
- status().isNotFound(),
- content().json("{\"message\":\"User not found.\"}"));
- }
-
- @Test
- public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
- doThrow(BadRequestException.create("Not allowed")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpectAll(
- status().isBadRequest(),
- content().json("{\"message\":\"Not allowed\"}"));
- }
-
- @Test
- public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
- when(webAppContext.getBean(DbClient.class).userDao()).thenReturn(mock(UserDao.class));
- when(webAppContext.getBean(UserSession.class).getLogin()).thenReturn("userToDelete");
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpectAll(
- status().isBadRequest(),
- content().json("{\"message\":\"Self-deactivation is not possible\"}"));
- }
-
- @Test
- public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
- .andExpect(
- status().isBadRequest());
- }
-
- @Test
- public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
- .andExpect(status().isNoContent());
-
- verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
- }
-
- @Test
- public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=false"))
- .andExpect(status().isNoContent());
-
- verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
- }
-
- @Test
- public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
- mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=true"))
- .andExpect(status().isNoContent());
-
- verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", true);
- }
-}
+++ /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 org.junit.Before;
-import org.junit.runner.RunWith;
-import org.sonar.server.v2.config.MockConfigForControllers;
-import org.sonar.server.v2.config.PlatformLevel4WebConfig;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.context.web.WebAppConfiguration;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.context.WebApplicationContext;
-
-@WebAppConfiguration
-@ContextConfiguration(
- classes = {PlatformLevel4WebConfig.class, MockConfigForControllers.class}
-)
-@RunWith(SpringRunner.class)
-public abstract class ControllerIT {
- @Autowired
- protected WebApplicationContext webAppContext;
-
- protected MockMvc mockMvc;
-
- @Before
- public void setup() {
- mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
- }
-}
+++ /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.db.DbClient;
-import org.sonar.server.common.user.service.UserService;
-import org.sonar.server.health.CeStatusNodeCheck;
-import org.sonar.server.health.DbConnectionNodeCheck;
-import org.sonar.server.health.EsStatusNodeCheck;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.health.WebServerStatusNodeCheck;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.platform.ws.LivenessChecker;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.user.UserUpdater;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-import static org.mockito.Mockito.mock;
-
-@Configuration
-public class MockConfigForControllers {
-
- @Bean
- public DbClient dbClient() {
- return mock(DbClient.class);
- }
-
- @Bean
- public DbConnectionNodeCheck dbConnectionNodeCheck() {
- return mock(DbConnectionNodeCheck.class);
- }
-
- @Bean
- public WebServerStatusNodeCheck webServerStatusNodeCheck() {
- return mock(WebServerStatusNodeCheck.class);
- }
-
- @Bean
- public CeStatusNodeCheck ceStatusNodeCheck() {
- return mock(CeStatusNodeCheck.class);
- }
-
- @Bean
- public EsStatusNodeCheck esStatusNodeCheck() {
- return mock(EsStatusNodeCheck.class);
- }
-
- @Bean
- public LivenessChecker livenessChecker() {
- return mock(LivenessChecker.class);
- }
-
- @Bean
- public HealthChecker healthChecker() {
- return mock(HealthChecker.class);
- }
-
- @Bean
- public SystemPasscode systemPasscode() {
- return mock(SystemPasscode.class);
- }
-
- @Bean
- public NodeInformation nodeInformation() {
- return mock(NodeInformation.class);
- }
-
- @Bean
- public UserSession userSession() {
- return mock(UserSession.class);
- }
-
- @Bean
- UserUpdater userUpdater() {
- return mock(UserUpdater.class);
- }
-
- @Bean
- UserService userService() {
- return mock(UserService.class);
- }
-
-}
+++ /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.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-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.ControllerIT;
-
-import static org.mockito.Mockito.when;
-import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
-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.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-public class DefaultLivenessControllerIT extends ControllerIT {
-
- private static final String VALID_PASSCODE = "valid_passcode";
- private static final String INVALID_PASSCODE = "invalid_passcode";
-
- @Before
- public void setup() {
- super.setup();
- when(webAppContext.getBean(LivenessChecker.class).liveness()).thenReturn(true);
- }
-
- @After
- public void resetUsedMocks() {
- Mockito.reset(webAppContext.getBean(SystemPasscode.class));
- Mockito.reset(webAppContext.getBean(UserSession.class));
- }
-
- @Test
- public void getSystemLiveness_whenValidPasscode_shouldSucceed() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
-
- mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
- .andExpect(status().isNoContent());
- }
-
- @Test
- public void getSystemLiveness_whenAdminCredential_shouldSucceed() throws Exception {
- when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true);
-
- mockMvc.perform(get(LIVENESS_ENDPOINT))
- .andExpect(status().isNoContent());
- }
-
- @Test
- public void getSystemLiveness_whenNoUserSessionAndNoPasscode_shouldReturnForbidden() throws Exception {
- mockMvc.perform(get(LIVENESS_ENDPOINT))
- .andExpectAll(
- status().isForbidden(),
- content().json("{\"message\":\"Insufficient privileges\"}"));
- }
-
- @Test
- public void getSystemLiveness_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
- when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(false);
-
- mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
- .andExpectAll(
- status().isForbidden(),
- content().json("{\"message\":\"Insufficient privileges\"}"));
- }
-
- @Test
- public void getSystemLiveness_whenLivenessCheckFails_shouldReturnServerError() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
- when(webAppContext.getBean(LivenessChecker.class).liveness()).thenReturn(false);
-
- mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
- .andExpectAll(
- status().isInternalServerError(),
- content().json("{\"message\":\"Liveness check failed\"}"));
- }
-}
+++ /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.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.health.Health;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.v2.common.ControllerIT;
-
-import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
-import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-
-public class HealthControllerIT extends ControllerIT {
-
- private static final String VALID_PASSCODE = "valid_passcode";
- private static final String INVALID_PASSCODE = "invalid_passcode";
- private static final Health HEALTH_RESULT = Health.builder().
- setStatus(Health.Status.YELLOW)
- .addCause("One cause")
- .build();
-
- @Before
- public void setup() {
- super.setup();
- when(webAppContext.getBean(HealthChecker.class).checkNode()).thenReturn(HEALTH_RESULT);
- }
-
- @After
- public void resetUsedMocks() {
- Mockito.reset(webAppContext.getBean(SystemPasscode.class));
- Mockito.reset(webAppContext.getBean(NodeInformation.class));
- Mockito.reset(webAppContext.getBean(UserSession.class));
- }
-
- @Test
- public void getSystemHealth_whenValidPasscodeAndStandaloneMode_shouldSucceed() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
- when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(true);
-
- mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
- .andExpectAll(
- status().isOk(),
- content().json("""
- {
- "status":"YELLOW",
- "causes":[
- "One cause"
- ]
- }"""));
- }
-
- @Test
- public void getSystemHealth_whenAdminCredentialAndStandaloneMode_shouldSucceed() throws Exception {
- when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true);
- when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(true);
-
- mockMvc.perform(get(HEALTH_ENDPOINT))
- .andExpectAll(
- status().isOk(),
- content().json("""
- {
- "status":"YELLOW",
- "causes":[
- "One cause"
- ]
- }"""));
- }
-
- @Test
- public void getSystemHealth_whenNoCredentials_shouldReturnForbidden() throws Exception {
- mockMvc.perform(get(HEALTH_ENDPOINT))
- .andExpectAll(
- status().isForbidden(),
- content().json("{\"message\":\"Insufficient privileges\"}"));
- }
-
- @Test
- public void getSystemHealth_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
- when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(false);
-
- mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
- .andExpectAll(
- status().isForbidden(),
- content().json("{\"message\":\"Insufficient privileges\"}"));
- }
-
- @Test
- public void getSystemHealth_whenUnauthorizedExceptionThrown_shouldReturnUnauthorized() throws Exception {
- when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenThrow(new UnauthorizedException("UnauthorizedException"));
-
- mockMvc.perform(get(HEALTH_ENDPOINT))
- .andExpectAll(
- status().isUnauthorized(),
- content().json("{\"message\":\"UnauthorizedException\"}"));
- }
-
- @Test
- public void getSystemHealth_whenValidPasscodeAndClusterMode_shouldReturnNotImplemented() throws Exception {
- when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
- when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(false);
-
- mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
- .andExpectAll(
- status().is(HTTP_NOT_IMPLEMENTED),
- content().json("{\"message\":\"Unsupported in cluster mode\"}"));
- }
-}
public class WebApiEndpoints {
private static final String SYSTEM_ENDPOINTS = "/system";
-
public static final String LIVENESS_ENDPOINT = SYSTEM_ENDPOINTS + "/liveness";
-
public static final String HEALTH_ENDPOINT = SYSTEM_ENDPOINTS + "/health";
public static final String USER_ENDPOINT = "/users";
) {
@VisibleForTesting
- static final String DEFAULT_PAGE_SIZE = "50";
+ public static final String DEFAULT_PAGE_SIZE = "50";
@VisibleForTesting
- static final String DEFAULT_PAGE_INDEX = "1";
+ public static final String DEFAULT_PAGE_INDEX = "1";
public RestPage(@Nullable Integer pageSize, @Nullable Integer pageIndex) {
this.pageSize = pageSize == null ? Integer.valueOf(DEFAULT_PAGE_SIZE) : pageSize;
--- /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.api.system.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;
+
+public class DefaultLivenessController implements LivenessController {
+
+ private final LivenessChecker livenessChecker;
+ private final UserSession userSession;
+ private final SystemPasscode systemPasscode;
+
+ public DefaultLivenessController(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.api.system.controller;
+
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.health.Health;
+import org.sonar.server.health.HealthChecker;
+import org.sonar.server.platform.NodeInformation;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+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.RestController;
+
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
+
+/*
+This controller does not support the cluster mode.
+This is not the final implementation, as we have to first define what are endpoint contracts.
+*/
+@RestController
+@RequestMapping(HEALTH_ENDPOINT)
+public class HealthController {
+
+ private final HealthChecker healthChecker;
+ private final SystemPasscode systemPasscode;
+ private final NodeInformation nodeInformation;
+ private final UserSession userSession;
+
+ public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode, NodeInformation nodeInformation,
+ UserSession userSession) {
+ this.healthChecker = healthChecker;
+ this.systemPasscode = systemPasscode;
+ this.nodeInformation = nodeInformation;
+ this.userSession = userSession;
+ }
+
+ public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode) {
+ this(healthChecker, systemPasscode, null, null);
+ }
+
+ @GetMapping
+ public Health getHealth(@RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode) {
+ if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
+ return getHealth();
+ }
+ throw new ForbiddenException("Insufficient privileges");
+ }
+
+ private Health getHealth() {
+ if (nodeInformation == null || nodeInformation.isStandalone()) {
+ return healthChecker.checkNode();
+ }
+ throw new ServerException(HTTP_NOT_IMPLEMENTED, "Unsupported in cluster mode");
+ }
+
+ 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.api.system.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 org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
+
+@RequestMapping(LIVENESS_ENDPOINT)
+@RestController
+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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.system.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
import org.sonar.server.platform.ws.LivenessCheckerImpl;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
+import org.sonar.server.v2.api.system.controller.HealthController;
+import org.sonar.server.v2.api.system.controller.LivenessController;
import org.sonar.server.v2.api.user.controller.DefaultUserController;
import org.sonar.server.v2.api.user.controller.UserController;
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
-import org.sonar.server.v2.controller.DefautLivenessController;
-import org.sonar.server.v2.controller.HealthController;
-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;
@Bean
public LivenessController livenessController(LivenessChecker livenessChecker, UserSession userSession, SystemPasscode systemPasscode) {
- return new DefautLivenessController(livenessChecker, systemPasscode, userSession);
+ return new DefaultLivenessController(livenessChecker, systemPasscode, userSession);
}
@Bean
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.HealthController;
-import org.sonar.server.v2.controller.LivenessController;
+import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
+import org.sonar.server.v2.api.system.controller.HealthController;
+import org.sonar.server.v2.api.system.controller.LivenessController;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Bean
public LivenessController livenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode) {
- return new DefautLivenessController(livenessChecker, systemPasscode, null);
+ return new DefaultLivenessController(livenessChecker, systemPasscode, null);
}
@Bean
+++ /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.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-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;
-
-public class DefautLivenessController implements LivenessController {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(DefautLivenessController.class);
- 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 org.sonar.server.exceptions.ForbiddenException;
-import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.health.Health;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-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.RestController;
-
-import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
-import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
-
-/*
-This controller does not support the cluster mode.
-This is not the final implementation, as we have to first define what are endpoint contracts.
-*/
-@RestController
-@RequestMapping(HEALTH_ENDPOINT)
-public class HealthController {
-
- private final HealthChecker healthChecker;
- private final SystemPasscode systemPasscode;
- private final NodeInformation nodeInformation;
- private final UserSession userSession;
-
- public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode, NodeInformation nodeInformation,
- UserSession userSession) {
- this.healthChecker = healthChecker;
- this.systemPasscode = systemPasscode;
- this.nodeInformation = nodeInformation;
- this.userSession = userSession;
- }
-
- public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode) {
- this(healthChecker, systemPasscode, null, null);
- }
-
- @GetMapping
- public Health getHealth(@RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode) {
- if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
- return getHealth();
- }
- throw new ForbiddenException("Insufficient privileges");
- }
-
- private Health getHealth() {
- if (nodeInformation == null || nodeInformation.isStandalone()) {
- return healthChecker.checkNode();
- }
- throw new ServerException(HTTP_NOT_IMPLEMENTED, "Unsupported in cluster mode");
- }
-
- 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 org.springframework.web.bind.annotation.RestController;
-
-import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
-
-@RequestMapping(LIVENESS_ENDPOINT)
-@RestController
-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.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.server.v2.controller;
-
-import javax.annotation.ParametersAreNonnullByDefault;
--- /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.api;
+
+import org.sonar.server.v2.common.RestResponseEntityExceptionHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+public class ControllerTester {
+ public static MockMvc getMockMvc(Object... controllers) {
+ return MockMvcBuilders
+ .standaloneSetup(controllers)
+ .setControllerAdvice(new RestResponseEntityExceptionHandler())
+ .build();
+ }
+}
--- /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.api.system.controller;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
+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.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultLivenessControllerTest {
+
+ private static final String VALID_PASSCODE = "valid_passcode";
+ private static final String INVALID_PASSCODE = "invalid_passcode";
+
+ private final LivenessChecker livenessChecker = mock(LivenessChecker.class);
+ private final SystemPasscode systemPasscode = mock(SystemPasscode.class);
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultLivenessController(livenessChecker, systemPasscode, userSession));
+
+ @Test
+ public void getSystemLiveness_whenValidPasscode_shouldSucceed() throws Exception {
+ when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+ when(livenessChecker.liveness()).thenReturn(true);
+
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ public void getSystemLiveness_whenAdminCredential_shouldSucceed() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(livenessChecker.liveness()).thenReturn(true);
+
+ mockMvc.perform(get(LIVENESS_ENDPOINT))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ public void getSystemLiveness_whenNoUserSessionAndNoPasscode_shouldReturnForbidden() throws Exception {
+ mockMvc.perform(get(LIVENESS_ENDPOINT))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void getSystemLiveness_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
+ when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
+ userSession.logIn();
+
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void getSystemLiveness_whenLivenessCheckFails_shouldReturnServerError() throws Exception {
+ when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+ when(livenessChecker.liveness()).thenReturn(false);
+
+ mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+ .andExpectAll(
+ status().isInternalServerError(),
+ content().json("{\"message\":\"Liveness check failed\"}"));
+ }
+
+}
--- /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.api.system.controller;
+
+import com.google.gson.Gson;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.health.Health;
+import org.sonar.server.health.HealthChecker;
+import org.sonar.server.platform.NodeInformation;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
+import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+public class HealthControllerTest {
+
+ private static final String VALID_PASSCODE = "valid_passcode";
+ private static final String INVALID_PASSCODE = "invalid_passcode";
+ private static final Health HEALTH_RESULT = Health.builder().
+ setStatus(Health.Status.YELLOW)
+ .addCause("One cause")
+ .build();
+
+ private final HealthChecker healthChecker = mock(HealthChecker.class);
+ private final SystemPasscode systemPasscode = mock(SystemPasscode.class);
+ private final NodeInformation nodeInformation = mock(NodeInformation.class);
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new HealthController(healthChecker, systemPasscode, nodeInformation,userSession));
+
+
+ private static final Gson gson = new Gson();
+
+ @Test
+ public void getSystemHealth_whenValidPasscodeAndStandaloneMode_shouldSucceed() throws Exception {
+ when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+ when(nodeInformation.isStandalone()).thenReturn(true);
+ when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT);
+
+ MvcResult mvcResult = mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ Health actualHealth = gson.fromJson(mvcResult.getResponse().getContentAsString(), Health.class);
+ assertThat(actualHealth).isEqualTo(HEALTH_RESULT);
+ }
+
+ @Test
+ public void getSystemHealth_whenAdminCredentialAndStandaloneMode_shouldSucceed() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(nodeInformation.isStandalone()).thenReturn(true);
+ when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT);
+
+ MvcResult mvcResult = mockMvc.perform(get(HEALTH_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ Health actualHealth = gson.fromJson(mvcResult.getResponse().getContentAsString(), Health.class);
+ assertThat(actualHealth).isEqualTo(HEALTH_RESULT);
+ }
+
+ @Test
+ public void getSystemHealth_whenNoCredentials_shouldReturnForbidden() throws Exception {
+ mockMvc.perform(get(HEALTH_ENDPOINT))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void getSystemHealth_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
+ userSession.logIn();
+ when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
+
+ mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void getSystemHealth_whenValidPasscodeAndClusterMode_shouldReturnNotImplemented() throws Exception {
+ when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+ when(nodeInformation.isStandalone()).thenReturn(false);
+
+ mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+ .andExpectAll(
+ status().is(HTTP_NOT_IMPLEMENTED),
+ content().json("{\"message\":\"Unsupported in cluster mode\"}"));
+ }
+
+}
--- /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.api.user.controller;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserService;
+import org.sonar.server.common.user.service.UsersSearchRequest;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
+import org.sonar.server.v2.api.user.model.RestUser;
+import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_INDEX;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_SIZE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultUserControllerTest {
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ private final UserService userService = mock(UserService.class);
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultUserController(userSession, userService, new UsersSearchRestResponseGenerator(userSession)));
+
+ private static final Gson gson = new Gson();
+
+ @Test
+ public void search_whenNoParameters_shouldUseDefaultAndForwardToUserService() throws Exception {
+ when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
+
+ mockMvc.perform(get(USER_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
+ verify(userService).findUsers(requestCaptor.capture());
+ assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_SIZE));
+ assertThat(requestCaptor.getValue().getPage()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_INDEX));
+ assertThat(requestCaptor.getValue().isDeactivated()).isFalse();
+ }
+
+ @Test
+ public void search_whenParametersUsed_shouldForwardWithParameters() throws Exception {
+ when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("active", "false")
+ .param("managed", "true")
+ .param("q", "q")
+ .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
+ .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100")
+ .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
+ .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100")
+ .param("pageSize", "100")
+ .param("pageIndex", "2"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
+ verify(userService).findUsers(requestCaptor.capture());
+ assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(100);
+ assertThat(requestCaptor.getValue().getPage()).isEqualTo(2);
+ assertThat(requestCaptor.getValue().isDeactivated()).isTrue();
+ }
+
+ @Test
+ public void search_whenAdminParametersUsedButNotAdmin_shouldFail() throws Exception {
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"parameter sonarQubeLastConnectionDateFrom requires Administer System permission.\"}"));
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"parameter sonarQubeLastConnectionDateTo requires Administer System permission.\"}"));
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"parameter sonarLintLastConnectionDateFrom requires Administer System permission.\"}"));
+
+ mockMvc.perform(get(USER_ENDPOINT)
+ .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().string("{\"message\":\"parameter sonarLintLastConnectionDateTo requires Administer System permission.\"}"));
+ }
+
+ @Test
+ public void search_whenUserServiceReturnUsers_shouldReturnThem() throws Exception {
+ UserSearchResult user1 = generateUserSearchResult("user1", true, true, false, 2, 3);
+ UserSearchResult user2 = generateUserSearchResult("user2", true, false, false, 3, 0);
+ UserSearchResult user3 = generateUserSearchResult("user3", true, false, true, 1, 1);
+ UserSearchResult user4 = generateUserSearchResult("user4", false, true, false, 0, 0);
+ List<UserSearchResult> users = List.of(user1, user2, user3, user4);
+ when(userService.findUsers(any())).thenReturn(new SearchResults<>(users, users.size()));
+ userSession.logIn().setSystemAdministrator();
+
+ MvcResult mvcResult = mockMvc.perform(get(USER_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ UsersSearchRestResponse actualUsersSearchRestResponse = gson.fromJson(mvcResult.getResponse().getContentAsString(), UsersSearchRestResponse.class);
+ assertThat(actualUsersSearchRestResponse.users())
+ .containsExactlyInAnyOrder(toRestUser(user1), toRestUser(user2), toRestUser(user3), toRestUser(user4));
+ assertThat(actualUsersSearchRestResponse.pageRestResponse().total()).isEqualTo(users.size());
+
+ }
+
+ private UserSearchResult generateUserSearchResult(String id, boolean active, boolean local, boolean managed, int groupsCount, int tokensCount) {
+ UserDto userDto = new UserDto()
+ .setLogin("login_" + id)
+ .setUuid("uuid_" + id)
+ .setName("name_" + id)
+ .setEmail(id + "@email.com")
+ .setActive(active)
+ .setLocal(local)
+ .setExternalLogin("externalLogin_" + id)
+ .setExternalId("externalId_" + id)
+ .setExternalIdentityProvider("externalIdentityProvider_" + id)
+ .setLastConnectionDate(0L)
+ .setLastSonarlintConnectionDate(1L);
+
+ List<String> groups = new ArrayList<>();
+ IntStream.range(1, groupsCount).forEach(i -> groups.add("group" + i));
+
+ return new UserSearchResult(userDto, managed, Optional.of("avatar_" + id), groups, tokensCount);
+ }
+
+ private RestUser toRestUser(UserSearchResult userSearchResult) {
+ return new RestUser(
+ userSearchResult.userDto().getLogin(),
+ userSearchResult.userDto().getLogin(),
+ userSearchResult.userDto().getName(),
+ userSearchResult.userDto().getEmail(),
+ userSearchResult.userDto().isActive(),
+ userSearchResult.userDto().isLocal(),
+ userSearchResult.managed(),
+ userSearchResult.userDto().getExternalLogin(),
+ userSearchResult.userDto().getExternalIdentityProvider(),
+ userSearchResult.avatar().orElse(""),
+ formatDateTime(userSearchResult.userDto().getLastConnectionDate()),
+ formatDateTime(userSearchResult.userDto().getLastSonarlintConnectionDate()),
+ userSearchResult.groups().size(),
+ userSearchResult.tokensCount()
+ );
+ }
+
+ @Test
+ public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ doThrow(new NotFoundException("User not found.")).when(userService).deactivate("userToDelete", false);
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+ .andExpectAll(
+ status().isNotFound(),
+ content().json("{\"message\":\"User not found.\"}"));
+ }
+
+ @Test
+ public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ doThrow(BadRequestException.create("Not allowed")).when(userService).deactivate("userToDelete", false);
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"Not allowed\"}"));
+ }
+
+ @Test
+ public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
+ userSession.logIn("userToDelete").setSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json("{\"message\":\"Self-deactivation is not possible\"}"));
+ }
+
+ @Test
+ public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
+ .andExpect(
+ status().isBadRequest());
+ }
+
+ @Test
+ public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+ .andExpect(status().isNoContent());
+
+ verify(userService).deactivate("userToDelete", false);
+ }
+
+ @Test
+ public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "false"))
+ .andExpect(status().isNoContent());
+
+ verify(userService).deactivate("userToDelete", false);
+ }
+
+ @Test
+ public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "true"))
+ .andExpect(status().isNoContent());
+
+ verify(userService).deactivate("userToDelete", true);
+ }
+}