diff options
author | Aurelien Poscia <aurelien.poscia@sonarsource.com> | 2023-07-19 11:38:55 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-26 20:03:24 +0000 |
commit | b0ab2c391e4259d9b90423b42b39a5e51f4f6008 (patch) | |
tree | 05bfd5a89fcfcb1ac63fde55c3c728bb86082d08 /server/sonar-webserver-webapi-v2 | |
parent | 3a6f4755225e09971e99d7e75ea1b97a85634084 (diff) | |
download | sonarqube-b0ab2c391e4259d9b90423b42b39a5e51f4f6008.tar.gz sonarqube-b0ab2c391e4259d9b90423b42b39a5e51f4f6008.zip |
SONAR-19963 Add GET /api/v2/users endpoint
Diffstat (limited to 'server/sonar-webserver-webapi-v2')
29 files changed, 1010 insertions, 26 deletions
diff --git a/server/sonar-webserver-webapi-v2/build.gradle b/server/sonar-webserver-webapi-v2/build.gradle index c206e6616a2..42a33235113 100644 --- a/server/sonar-webserver-webapi-v2/build.gradle +++ b/server/sonar-webserver-webapi-v2/build.gradle @@ -6,11 +6,15 @@ sonarqube { dependencies { // please keep the list grouped by configuration and ordered by name - api 'org.springdoc:springdoc-openapi-ui' + api 'org.springdoc:springdoc-openapi-webmvc-core' api 'org.springframework:spring-webmvc' + api 'org.hibernate:hibernate-validator' + api 'javax.el:javax.el-api' + api 'org.glassfish:javax.el' 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-common') + // We are not supposed 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' diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java new file mode 100644 index 00000000000..9073d3b25b8 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java @@ -0,0 +1,28 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DefaultUserControllerTest { + +} diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java index a5871772bf0..12bfee0eb87 100644 --- a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java +++ b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java @@ -20,6 +20,7 @@ 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; @@ -98,4 +99,9 @@ public class MockConfigForControllers { ManagedInstanceChecker managedInstanceChecker() { return mock(ManagedInstanceChecker.class); } + + @Bean + UserService userService() { + return mock(UserService.class); + } } diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java index eb7eed3fe32..3e773445335 100644 --- a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java +++ b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java @@ -73,7 +73,7 @@ public class DefaultLivenessControllerIT extends ControllerIT { mockMvc.perform(get(LIVENESS_ENDPOINT)) .andExpectAll( status().isForbidden(), - content().string("Insufficient privileges")); + content().json("{\"message\":\"Insufficient privileges\"}")); } @Test @@ -84,7 +84,7 @@ public class DefaultLivenessControllerIT extends ControllerIT { mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) .andExpectAll( status().isForbidden(), - content().string("Insufficient privileges")); + content().json("{\"message\":\"Insufficient privileges\"}")); } @Test @@ -95,6 +95,6 @@ public class DefaultLivenessControllerIT extends ControllerIT { mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE)) .andExpectAll( status().isInternalServerError(), - content().string("Liveness check failed")); + content().json("{\"message\":\"Liveness check failed\"}")); } } diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java index c216fd43c2f..9fb213f9090 100644 --- a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java +++ b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java @@ -101,7 +101,7 @@ public class HealthControllerIT extends ControllerIT { mockMvc.perform(get(HEALTH_ENDPOINT)) .andExpectAll( status().isForbidden(), - content().string("Insufficient privileges")); + content().json("{\"message\":\"Insufficient privileges\"}")); } @Test @@ -112,7 +112,7 @@ public class HealthControllerIT extends ControllerIT { mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) .andExpectAll( status().isForbidden(), - content().string("Insufficient privileges")); + content().json("{\"message\":\"Insufficient privileges\"}")); } @Test @@ -122,7 +122,7 @@ public class HealthControllerIT extends ControllerIT { mockMvc.perform(get(HEALTH_ENDPOINT)) .andExpectAll( status().isUnauthorized(), - content().string("UnauthorizedException")); + content().json("{\"message\":\"UnauthorizedException\"}")); } @Test @@ -133,6 +133,6 @@ public class HealthControllerIT extends ControllerIT { mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE)) .andExpectAll( status().is(HTTP_NOT_IMPLEMENTED), - content().string("Unsupported in cluster mode")); + content().json("{\"message\":\"Unsupported in cluster mode\"}")); } } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java index 89e3c797a55..4870e06292f 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java @@ -25,6 +25,7 @@ public class WebApiEndpoints { 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"; private WebApiEndpoints() { } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestError.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestError.java new file mode 100644 index 00000000000..534467a85cd --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestError.java @@ -0,0 +1,22 @@ +/* + * 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.model; + +public record RestError(String message) {} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestPage.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestPage.java new file mode 100644 index 00000000000..ec374ec5f36 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestPage.java @@ -0,0 +1,54 @@ +/* + * 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.model; + +import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.Positive; +import org.jetbrains.annotations.Nullable; + +public record RestPage( + @Min(1) + @Max(500) + @Parameter( + description = "Number of results per page", + schema = @Schema(defaultValue = DEFAULT_PAGE_SIZE, implementation = Integer.class)) + Integer pageSize, + @Positive + @Parameter( + description = "1-based page number", + schema = @Schema(defaultValue = DEFAULT_PAGE_INDEX, implementation = Integer.class)) + Integer pageIndex +) { + + @VisibleForTesting + static final String DEFAULT_PAGE_SIZE = "50"; + @VisibleForTesting + 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; + this.pageIndex = pageIndex == null ? Integer.valueOf(DEFAULT_PAGE_INDEX) : pageIndex; + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/package-info.java new file mode 100644 index 00000000000..381c1e0b60f --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.model; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/PageRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/PageRestResponse.java new file mode 100644 index 00000000000..13b5f050668 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/PageRestResponse.java @@ -0,0 +1,23 @@ +/* + * 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.response; + +public record PageRestResponse(int pageIndex, int pageSize, int total) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/package-info.java new file mode 100644 index 00000000000..42ee281a5fc --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.response; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java new file mode 100644 index 00000000000..c095a720cd1 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java @@ -0,0 +1,90 @@ +/* + * 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 java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.utils.Paging; +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.ForbiddenException; +import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.model.RestPage; +import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator; +import org.sonar.server.v2.api.user.request.UsersSearchRestRequest; +import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; + +import static org.sonar.api.utils.Paging.forPageIndex; + +public class DefaultUserController implements UserController { + private final UsersSearchRestResponseGenerator usersSearchResponseGenerator; + private final UserService userService; + private final UserSession userSession; + + public DefaultUserController(UserSession userSession, UserService userService, UsersSearchRestResponseGenerator usersSearchResponseGenerator) { + this.userSession = userSession; + this.usersSearchResponseGenerator = usersSearchResponseGenerator; + this.userService = userService; + } + + @Override + public UsersSearchRestResponse search(UsersSearchRestRequest usersSearchRestRequest, RestPage page) { + throwIfAdminOnlyParametersAreUsed(usersSearchRestRequest); + + SearchResults<UserSearchResult> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, page)); + Paging paging = forPageIndex(page.pageIndex()).withPageSize(page.pageSize()).andTotal(userSearchResults.total()); + + return usersSearchResponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging); + } + + private void throwIfAdminOnlyParametersAreUsed(UsersSearchRestRequest usersSearchRestRequest) { + if (!userSession.isSystemAdministrator()) { + throwIfValuePresent("sonarLintLastConnectionDateFrom", usersSearchRestRequest.sonarLintLastConnectionDateFrom()); + throwIfValuePresent("sonarLintLastConnectionDateTo", usersSearchRestRequest.sonarLintLastConnectionDateTo()); + throwIfValuePresent("sonarQubeLastConnectionDateFrom", usersSearchRestRequest.sonarQubeLastConnectionDateFrom()); + throwIfValuePresent("sonarQubeLastConnectionDateTo", usersSearchRestRequest.sonarQubeLastConnectionDateTo()); + } + } + + private static void throwIfValuePresent(String parameter, @Nullable Object value) { + Optional.ofNullable(value).ifPresent(v -> throwForbiddenFor(parameter)); + } + + private static void throwForbiddenFor(String parameterName) { + throw new ForbiddenException("parameter " + parameterName + " requires Administer System permission."); + } + + private static UsersSearchRequest toUserSearchRequest(UsersSearchRestRequest usersSearchRestRequest, RestPage page) { + return UsersSearchRequest.builder() + .setDeactivated(Optional.ofNullable(usersSearchRestRequest.active()).map(active -> !active).orElse(false)) + .setManaged(usersSearchRestRequest.managed()) + .setQuery(usersSearchRestRequest.q()) + .setLastConnectionDateFrom(usersSearchRestRequest.sonarQubeLastConnectionDateFrom()) + .setLastConnectionDateTo(usersSearchRestRequest.sonarQubeLastConnectionDateTo()) + .setSonarLintLastConnectionDateFrom(usersSearchRestRequest.sonarLintLastConnectionDateFrom()) + .setSonarLintLastConnectionDateTo(usersSearchRestRequest.sonarLintLastConnectionDateTo()) + .setPage(page.pageIndex()) + .setPageSize(page.pageSize()) + .build(); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java new file mode 100644 index 00000000000..03181b3438a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java @@ -0,0 +1,56 @@ +/* + * 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 io.swagger.v3.oas.annotations.Operation; +import javax.validation.Valid; +import org.sonar.server.v2.api.model.RestPage; +import org.sonar.server.v2.api.user.request.UsersSearchRestRequest; +import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +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.USER_ENDPOINT; + +@RequestMapping(USER_ENDPOINT) +@RestController +public interface UserController { + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Users search", description = """ + Get a list of users. By default, only active users are returned. + The following fields are only returned when user has Administer System permission or for logged-in in user : + 'email' + 'externalIdentity' + 'externalProvider' + 'groups' + 'lastConnectionDate' + 'sonarLintLastConnectionDate' + 'tokensCount' + Field 'sonarqubeLastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour. + """) + UsersSearchRestResponse search(@ParameterObject UsersSearchRestRequest usersSearchRestRequest, @Valid @ParameterObject RestPage restPage); +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/package-info.java new file mode 100644 index 00000000000..2aa60ec12dc --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.user.controller; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java new file mode 100644 index 00000000000..6ad0fad3341 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java @@ -0,0 +1,111 @@ +/* + * 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.converter; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.Nullable; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.Paging; +import org.sonar.db.user.UserDto; +import org.sonar.server.common.user.UsersSearchResponseGenerator; +import org.sonar.server.common.user.service.UserSearchResult; +import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.response.PageRestResponse; +import org.sonar.server.v2.api.user.model.RestUser; +import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; + +public class UsersSearchRestResponseGenerator implements UsersSearchResponseGenerator<UsersSearchRestResponse> { + + private final UserSession userSession; + + public UsersSearchRestResponseGenerator(UserSession userSession) { + this.userSession = userSession; + } + + @Override + public UsersSearchRestResponse toUsersForResponse(List<UserSearchResult> userSearchResults, Paging paging) { + List<RestUser> usersForResponse = toUsersForResponse(userSearchResults); + PageRestResponse pageRestResponse = new PageRestResponse(paging.pageIndex(), paging.pageSize(), paging.total()); + return new UsersSearchRestResponse(usersForResponse, pageRestResponse); + } + + private List<RestUser> toUsersForResponse(List<UserSearchResult> userSearchResults) { + return userSearchResults.stream() + .map(this::toUser) + .toList(); + } + + private RestUser toUser(UserSearchResult userSearchResult) { + UserDto userDto = userSearchResult.userDto(); + + String login = userDto.getLogin(); + String name = userDto.getName(); + String avatar = null; + Boolean active = null; + Boolean local = null; + String email = null; + String externalIdentityProvider = null; + String externalLogin = null; + Boolean managed = null; + String sqLastConnectionDate = null; + String slLastConnectionDate = null; + Integer groupSize = null; + Integer tokensCount = null; + + if (userSession.isLoggedIn()) { + avatar = userSearchResult.avatar().orElse(null); + active = userDto.isActive(); + local = userDto.isLocal(); + email = userDto.getEmail(); + externalIdentityProvider = userDto.getExternalIdentityProvider(); + } + if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), userDto.getUuid())) { + externalLogin = userDto.getExternalLogin(); + managed = userSearchResult.managed(); + sqLastConnectionDate = toDateTime(userDto.getLastConnectionDate()); + slLastConnectionDate = toDateTime(userDto.getLastSonarlintConnectionDate()); + groupSize = userSearchResult.groups().size(); + tokensCount = userSearchResult.tokensCount(); + } + + return new RestUser( + login, + login, + name, + email, + active, + local, + managed, + externalLogin, + externalIdentityProvider, + avatar, + sqLastConnectionDate, + slLastConnectionDate, + groupSize, + tokensCount + ); + } + + private static String toDateTime(@Nullable Long dateTimeMs) { + return Optional.ofNullable(dateTimeMs).map(DateUtils::formatDateTime).orElse(null); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/package-info.java new file mode 100644 index 00000000000..a44fcd944bf --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.user.converter; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUser.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUser.java new file mode 100644 index 00000000000..f3f7d0f2408 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUser.java @@ -0,0 +1,51 @@ +/* + * 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.model; + +import javax.annotation.Nullable; + +public record RestUser( + String id, + String login, + String name, + @Nullable + String email, + @Nullable + Boolean active, + @Nullable + Boolean local, + @Nullable + Boolean managed, + @Nullable + String externalLogin, + @Nullable + String externalProvider, + @Nullable + String avatar, + @Nullable + String sonarQubeLastConnectionDate, + @Nullable + String sonarLintLastConnectionDate, + @Nullable + Integer groupsCount, + @Nullable + Integer tokensCount +) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/package-info.java new file mode 100644 index 00000000000..713126161c3 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.user.model; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java new file mode 100644 index 00000000000..318424c8a85 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java @@ -0,0 +1,61 @@ +/* + * 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.request; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.annotation.Nullable; + +public record UsersSearchRestRequest( + @Parameter( + description = "Return active/inactive users", + schema = @Schema(defaultValue = "true", implementation = Boolean.class)) + Boolean active, + @Nullable + @Parameter(description = "Return managed or non-managed users. Only available for managed instances, throws for non-managed instances") + Boolean managed, + @Nullable + @Parameter(description = "Filter on login, name and email.\n" + + "This parameter can either perform an exact match, or a partial match (contains), it is case insensitive.") + String q, + @Nullable + @Parameter(description = "Filter the users based on the last connection date field. Only users who interacted with this instance at or after the date will be returned. " + + "The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", + example = "2020-01-01T00:00:00+0100") + String sonarQubeLastConnectionDateFrom, + @Nullable + @Parameter(description = "Filter the users based on the last connection date field. Only users that never connected or who interacted with this instance at " + + "or before the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", + example = "2020-01-01T00:00:00+0100") + String sonarQubeLastConnectionDateTo, + @Nullable + @Parameter(description = "Filter the users based on the sonar lint last connection date field Only users who interacted with this instance using SonarLint at or after " + + "the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", + example = "2020-01-01T00:00:00+0100") + String sonarLintLastConnectionDateFrom, + @Nullable + @Parameter(description = "Filter the users based on the sonar lint last connection date field. Only users that never connected or who interacted with this instance " + + "using SonarLint at or before the date will be returned. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)", + example = "2020-01-01T00:00:00+0100") + String sonarLintLastConnectionDateTo + +) { + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/package-info.java new file mode 100644 index 00000000000..cdde9822316 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.user.request; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UsersSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UsersSearchRestResponse.java new file mode 100644 index 00000000000..38e5a2dd897 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UsersSearchRestResponse.java @@ -0,0 +1,27 @@ +/* + * 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.response; + +import java.util.List; +import org.sonar.server.v2.api.response.PageRestResponse; +import org.sonar.server.v2.api.user.model.RestUser; + +public record UsersSearchRestResponse(List<RestUser> users, PageRestResponse pageRestResponse) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/package-info.java new file mode 100644 index 00000000000..d6b945f1666 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/package-info.java @@ -0,0 +1,23 @@ +/* + * 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.user.response; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java index d62cc5c6996..baccbbbc764 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java @@ -20,11 +20,16 @@ package org.sonar.server.v2.common; import java.util.Optional; +import java.util.stream.Collectors; +import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.ServerException; import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.v2.api.model.RestError; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -34,25 +39,29 @@ public class RestResponseEntityExceptionHandler { @ExceptionHandler(IllegalStateException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - protected ResponseEntity<Object> handleIllegalStateException(IllegalStateException illegalStateException) { - return new ResponseEntity<>(illegalStateException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + protected ResponseEntity<RestError> handleIllegalStateException(IllegalStateException illegalStateException) { + return new ResponseEntity<>(new RestError(illegalStateException.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } - @ExceptionHandler(ForbiddenException.class) - @ResponseStatus(HttpStatus.FORBIDDEN) - protected ResponseEntity<Object> handleForbiddenException(ForbiddenException forbiddenException) { - return handleServerException(forbiddenException); + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected ResponseEntity<RestError> handleBindException(BindException bindException) { + String validationErrors = bindException.getFieldErrors().stream() + .map(RestResponseEntityExceptionHandler::handleFieldError) + .collect(Collectors.joining()); + return new ResponseEntity<>(new RestError(validationErrors), HttpStatus.BAD_REQUEST); } - @ExceptionHandler(UnauthorizedException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - protected ResponseEntity<Object> handleUnauthorizedException(UnauthorizedException unauthorizedException) { - return handleServerException(unauthorizedException); + private static String handleFieldError(FieldError fieldError) { + String fieldName = fieldError.getField(); + String rejectedValueAsString = Optional.ofNullable(fieldError.getRejectedValue()).map(Object::toString).orElse("{}"); + String defaultMessage = fieldError.getDefaultMessage(); + return String.format("Value %s for field %s was rejected. Error: %s", rejectedValueAsString, fieldName, defaultMessage); } - - @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)); + @ExceptionHandler({ServerException.class, ForbiddenException.class, UnauthorizedException.class, BadRequestException.class}) + protected ResponseEntity<RestError> handleServerException(ServerException serverException) { + return new ResponseEntity<>(new RestError(serverException.getMessage()), + Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR)); } } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java index 34c77693a30..f8715068a36 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java @@ -22,10 +22,14 @@ package org.sonar.server.v2.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.sonar.server.v2.common.RestResponseEntityExceptionHandler; +import org.springdoc.core.SpringDocConfigProperties; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.http.MediaType; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @@ -33,6 +37,10 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; @ComponentScan(basePackages = {"org.springdoc"}) @PropertySource("classpath:springdoc.properties") public class CommonWebConfig { + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } @Bean public RestResponseEntityExceptionHandler restResponseEntityExceptionHandler() { @@ -45,9 +53,14 @@ public class CommonWebConfig { .info( new Info() .title("SonarQube Web API") - .version("0.0.1 alpha") + .version("1.0.0 beta") .description("Documentation of SonarQube Web API") ); } + @Bean + public BeanFactoryPostProcessor beanFactoryPostProcessor1(SpringDocConfigProperties springDocConfigProperties) { + return beanFactory -> springDocConfigProperties.setDefaultProducesMediaType(MediaType.APPLICATION_JSON_VALUE); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java index 60b82f70862..d6c133308df 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java @@ -20,6 +20,7 @@ package org.sonar.server.v2.config; import javax.annotation.Nullable; +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; @@ -30,6 +31,9 @@ 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.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; @@ -57,4 +61,15 @@ public class PlatformLevel4WebConfig { UserSession userSession) { return new HealthController(healthChecker, systemPasscode, nodeInformation, userSession); } + + @Bean + public UsersSearchRestResponseGenerator usersSearchResponseGenerator(UserSession userSession) { + return new UsersSearchRestResponseGenerator(userSession); + } + + @Bean + public UserController userController(UserSession userSession, UsersSearchRestResponseGenerator usersSearchResponseGenerator, UserService userService) { + return new DefaultUserController(userSession, userService, usersSearchResponseGenerator); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java index 8641f7f8811..d609c9e1110 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java @@ -20,15 +20,16 @@ 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; -import org.springframework.web.bind.annotation.RestController; -@RestController 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; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java index 3b4daf00620..9433a6e1b48 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java @@ -28,10 +28,12 @@ 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 diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/model/RestPageTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/model/RestPageTest.java new file mode 100644 index 00000000000..533b7714a12 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/model/RestPageTest.java @@ -0,0 +1,44 @@ +/* + * 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.model; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestPageTest { + + @Test + public void constructor_whenNoValueProvided_setsDefaultValues() { + RestPage restPage = new RestPage(null, null); + assertThat(restPage.pageIndex()).asString().isEqualTo(RestPage.DEFAULT_PAGE_INDEX); + assertThat(restPage.pageSize()).asString().isEqualTo(RestPage.DEFAULT_PAGE_SIZE); + } + + @Test + public void constructor_whenValuesProvided_useThem() { + RestPage restPage = new RestPage(10, 100); + assertThat(restPage.pageIndex()).isEqualTo(100); + assertThat(restPage.pageSize()).isEqualTo(10); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java new file mode 100644 index 00000000000..41bc8a916d3 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java @@ -0,0 +1,205 @@ +/* + * 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.converter; + +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.Nullable; +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.api.utils.DateUtils; +import org.sonar.api.utils.Paging; +import org.sonar.db.user.UserDto; +import org.sonar.server.common.user.service.UserSearchResult; +import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.response.PageRestResponse; +import org.sonar.server.v2.api.user.model.RestUser; +import org.sonar.server.v2.api.user.response.UsersSearchRestResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class UsersSearchRestResponseGeneratorTest { + + @Mock + private UserSession userSession; + + @InjectMocks + private UsersSearchRestResponseGenerator usersSearchRestResponseGenerator; + + @Test + public void toUsersForResponse_whenNoResults_mapsCorrectly() { + Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); + + UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(), paging); + + assertThat(usersForResponse.users()).isEmpty(); + assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); + } + + @Test + public void toUsersForResponse_whenAdmin_mapsAllFields() { + when(userSession.isLoggedIn()).thenReturn(true); + when(userSession.isSystemAdministrator()).thenReturn(true); + + Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); + + UserSearchResult userSearchResult1 = mockSearchResult(1, true); + UserSearchResult userSearchResult2 = mockSearchResult(2, false); + + UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); + + RestUser expectUser1 = buildExpectedResponseForAdmin(userSearchResult1); + RestUser expectUser2 = buildExpectedResponseForAdmin(userSearchResult2); + assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); + assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); + } + + private static RestUser buildExpectedResponseForAdmin(UserSearchResult userSearchResult) { + UserDto userDto = userSearchResult.userDto(); + return new RestUser( + userDto.getLogin(), + userDto.getLogin(), + userDto.getName(), + userDto.getEmail(), + userDto.isActive(), + userDto.isLocal(), + userSearchResult.managed(), + userDto.getExternalLogin(), + userDto.getExternalIdentityProvider(), + userSearchResult.avatar().orElse(null), + toDateTime(userDto.getLastConnectionDate()), + toDateTime(userDto.getLastSonarlintConnectionDate()), + userSearchResult.groups().size(), + userSearchResult.tokensCount() + ); + } + + @Test + public void toUsersForResponse_whenNonAdmin_mapsNonAdminFields() { + when(userSession.isLoggedIn()).thenReturn(true); + + Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); + + UserSearchResult userSearchResult1 = mockSearchResult(1, true); + UserSearchResult userSearchResult2 = mockSearchResult(2, false); + + UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); + + RestUser expectUser1 = buildExpectedResponseForUser(userSearchResult1); + RestUser expectUser2 = buildExpectedResponseForUser(userSearchResult2); + assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); + assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); + } + + private static RestUser buildExpectedResponseForUser(UserSearchResult userSearchResult) { + UserDto userDto = userSearchResult.userDto(); + return new RestUser( + userDto.getLogin(), + userDto.getLogin(), + userDto.getName(), + userDto.getEmail(), + userDto.isActive(), + userDto.isLocal(), + null, + null, + userDto.getExternalIdentityProvider(), + userSearchResult.avatar().orElse(null), + null, + null, + null, + null + ); + } + + @Test + public void toUsersForResponse_whenAnonymous_returnsOnlyNameAndLogin() { + Paging paging = Paging.forPageIndex(1).withPageSize(2).andTotal(3); + + UserSearchResult userSearchResult1 = mockSearchResult(1, true); + UserSearchResult userSearchResult2 = mockSearchResult(2, false); + + UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging); + + RestUser expectUser1 = buildExpectedResponseForAnonymous(userSearchResult1); + RestUser expectUser2 = buildExpectedResponseForAnonymous(userSearchResult2); + assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2); + assertPaginationInformationAreCorrect(paging, usersForResponse.pageRestResponse()); + } + + private static RestUser buildExpectedResponseForAnonymous(UserSearchResult userSearchResult) { + UserDto userDto = userSearchResult.userDto(); + return new RestUser( + userDto.getLogin(), + userDto.getLogin(), + userDto.getName(), + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + } + + private static String toDateTime(@Nullable Long dateTimeMs) { + return Optional.ofNullable(dateTimeMs).map(DateUtils::formatDateTime).orElse(null); + } + + private static UserSearchResult mockSearchResult(int i, boolean booleanFlagsValue) { + UserSearchResult userSearchResult = mock(UserSearchResult.class, RETURNS_DEEP_STUBS); + UserDto user1 = new UserDto() + .setUuid("uuid_" + i) + .setLogin("login_" + i) + .setName("name_" + i) + .setEmail("email@" + i) + .setExternalId("externalId" + i) + .setExternalLogin("externalLogin" + 1) + .setExternalIdentityProvider("exernalIdp_" + i) + .setLastConnectionDate(100L + i) + .setLastSonarlintConnectionDate(200L + i) + .setLocal(booleanFlagsValue) + .setActive(booleanFlagsValue); + + when(userSearchResult.userDto()).thenReturn(user1); + when(userSearchResult.managed()).thenReturn(booleanFlagsValue); + when(userSearchResult.tokensCount()).thenReturn(i); + when(userSearchResult.groups().size()).thenReturn(i * 100); + return userSearchResult; + } + + private static void assertPaginationInformationAreCorrect(Paging paging, PageRestResponse pageRestResponse) { + assertThat(pageRestResponse.pageIndex()).isEqualTo(paging.pageIndex()); + assertThat(pageRestResponse.pageSize()).isEqualTo(paging.pageSize()); + assertThat(pageRestResponse.total()).isEqualTo(paging.total()); + } + +} |