aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-webapi-v2
diff options
context:
space:
mode:
authorAurelien Poscia <aurelien.poscia@sonarsource.com>2023-07-19 11:38:55 +0200
committersonartech <sonartech@sonarsource.com>2023-07-26 20:03:24 +0000
commitb0ab2c391e4259d9b90423b42b39a5e51f4f6008 (patch)
tree05bfd5a89fcfcb1ac63fde55c3c728bb86082d08 /server/sonar-webserver-webapi-v2
parent3a6f4755225e09971e99d7e75ea1b97a85634084 (diff)
downloadsonarqube-b0ab2c391e4259d9b90423b42b39a5e51f4f6008.tar.gz
sonarqube-b0ab2c391e4259d9b90423b42b39a5e51f4f6008.zip
SONAR-19963 Add GET /api/v2/users endpoint
Diffstat (limited to 'server/sonar-webserver-webapi-v2')
-rw-r--r--server/sonar-webserver-webapi-v2/build.gradle8
-rw-r--r--server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java28
-rw-r--r--server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java8
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestError.java22
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestPage.java54
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/PageRestResponse.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/response/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java90
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java56
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java111
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUser.java51
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UsersSearchRestRequest.java61
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/UsersSearchRestResponse.java27
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/response/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java37
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/CommonWebConfig.java15
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java15
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java5
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/model/RestPageTest.java44
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java205
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());
+ }
+
+}