You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearchAction.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.user.ws;
  21. import java.util.Optional;
  22. import org.sonar.api.server.ws.Change;
  23. import org.sonar.api.server.ws.Request;
  24. import org.sonar.api.server.ws.Response;
  25. import org.sonar.api.server.ws.WebService;
  26. import org.sonar.api.utils.Paging;
  27. import org.sonar.server.common.SearchResults;
  28. import org.sonar.server.common.user.service.UserSearchResult;
  29. import org.sonar.server.common.user.service.UserService;
  30. import org.sonar.server.common.user.service.UsersSearchRequest;
  31. import org.sonar.server.es.SearchOptions;
  32. import org.sonar.server.exceptions.ServerException;
  33. import org.sonar.server.user.UserSession;
  34. import org.sonarqube.ws.Users;
  35. import static com.google.common.base.Preconditions.checkArgument;
  36. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  37. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  38. import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
  39. import static org.sonar.api.utils.Paging.forPageIndex;
  40. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  41. public class SearchAction implements UsersWsAction {
  42. private static final String DEACTIVATED_PARAM = "deactivated";
  43. private static final String MANAGED_PARAM = "managed";
  44. private static final int MAX_PAGE_SIZE = 500;
  45. static final String LAST_CONNECTION_DATE_FROM = "lastConnectedAfter";
  46. static final String LAST_CONNECTION_DATE_TO = "lastConnectedBefore";
  47. static final String SONAR_LINT_LAST_CONNECTION_DATE_FROM = "slLastConnectedAfter";
  48. static final String SONAR_LINT_LAST_CONNECTION_DATE_TO = "slLastConnectedBefore";
  49. private final UserSession userSession;
  50. private final UserService userService;
  51. private final SearchWsReponseGenerator searchWsReponseGenerator;
  52. public SearchAction(UserSession userSession,
  53. UserService userService, SearchWsReponseGenerator searchWsReponseGenerator) {
  54. this.userSession = userSession;
  55. this.userService = userService;
  56. this.searchWsReponseGenerator = searchWsReponseGenerator;
  57. }
  58. @Override
  59. public void define(WebService.NewController controller) {
  60. WebService.NewAction action = controller.createAction("search")
  61. .setDescription("Get a list of users. By default, only active users are returned.<br/>" +
  62. "The following fields are only returned when user has Administer System permission or for logged-in in user :" +
  63. "<ul>" +
  64. " <li>'email'</li>" +
  65. " <li>'externalIdentity'</li>" +
  66. " <li>'externalProvider'</li>" +
  67. " <li>'groups'</li>" +
  68. " <li>'lastConnectionDate'</li>" +
  69. " <li>'sonarLintLastConnectionDate'</li>" +
  70. " <li>'tokensCount'</li>" +
  71. "</ul>" +
  72. "Field 'lastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.")
  73. .setSince("3.6")
  74. .setChangelog(
  75. new Change("10.1", "New optional parameters " + SONAR_LINT_LAST_CONNECTION_DATE_FROM +
  76. " and " + SONAR_LINT_LAST_CONNECTION_DATE_TO + " to filter users by SonarLint last connection date. Only available with Administer System permission."),
  77. new Change("10.1", "New optional parameters " + LAST_CONNECTION_DATE_FROM +
  78. " and " + LAST_CONNECTION_DATE_TO + " to filter users by SonarQube last connection date. Only available with Administer System permission."),
  79. new Change("10.1", "New field 'sonarLintLastConnectionDate' is added to response"),
  80. new Change("10.0", "'q' parameter values is now always performing a case insensitive match"),
  81. new Change("10.0", "New parameter 'managed' to optionally search by managed status"),
  82. new Change("10.0", "Response includes 'managed' field."),
  83. new Change("9.7", "New parameter 'deactivated' to optionally search for deactivated users"),
  84. new Change("7.7", "New field 'lastConnectionDate' is added to response"),
  85. new Change("7.4", "External identity is only returned to system administrators"),
  86. new Change("6.4", "Paging response fields moved to a Paging object"),
  87. new Change("6.4", "Avatar has been added to the response"),
  88. new Change("6.4", "Email is only returned when user has Administer System permission"))
  89. .setHandler(this)
  90. .setResponseExample(getClass().getResource("search-example.json"));
  91. action.addPagingParams(50, SearchOptions.MAX_PAGE_SIZE);
  92. final String dateExample = "2020-01-01T00:00:00+0100";
  93. action.createParam(TEXT_QUERY)
  94. .setMinimumLength(2)
  95. .setDescription("Filter on login, name and email.<br />" +
  96. "This parameter can either perform an exact match, or a partial match (contains), it is case insensitive.");
  97. action.createParam(DEACTIVATED_PARAM)
  98. .setSince("9.7")
  99. .setDescription("Return deactivated users instead of active users")
  100. .setRequired(false)
  101. .setDefaultValue(false)
  102. .setBooleanPossibleValues();
  103. action.createParam(MANAGED_PARAM)
  104. .setSince("10.0")
  105. .setDescription("Return managed or non-managed users. Only available for managed instances, throws for non-managed instances.")
  106. .setRequired(false)
  107. .setDefaultValue(null)
  108. .setBooleanPossibleValues();
  109. action.createParam(LAST_CONNECTION_DATE_FROM)
  110. .setSince("10.1")
  111. .setDescription("""
  112. Filter the users based on the last connection date field.
  113. Only users who interacted with this instance at or after the date will be returned.
  114. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""")
  115. .setRequired(false)
  116. .setDefaultValue(null)
  117. .setExampleValue(dateExample);
  118. action.createParam(LAST_CONNECTION_DATE_TO)
  119. .setSince("10.1")
  120. .setDescription("""
  121. Filter the users based on the last connection date field.
  122. Only users that never connected or who interacted with this instance at or before the date will be returned.
  123. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""")
  124. .setRequired(false)
  125. .setDefaultValue(null)
  126. .setExampleValue(dateExample);
  127. action.createParam(SONAR_LINT_LAST_CONNECTION_DATE_FROM)
  128. .setSince("10.1")
  129. .setDescription("""
  130. Filter the users based on the sonar lint last connection date field
  131. Only users who interacted with this instance using SonarLint at or after the date will be returned.
  132. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""")
  133. .setRequired(false)
  134. .setDefaultValue(null)
  135. .setExampleValue(dateExample);
  136. action.createParam(SONAR_LINT_LAST_CONNECTION_DATE_TO)
  137. .setSince("10.1")
  138. .setDescription("""
  139. Filter the users based on the sonar lint last connection date field.
  140. Only users that never connected or who interacted with this instance using SonarLint at or before the date will be returned.
  141. The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""")
  142. .setRequired(false)
  143. .setDefaultValue(null)
  144. .setExampleValue(dateExample);
  145. }
  146. @Override
  147. public void handle(Request request, Response response) throws Exception {
  148. throwIfAdminOnlyParametersAreUsed(request);
  149. Users.SearchWsResponse wsResponse = doHandle(toSearchRequest(request));
  150. writeProtobuf(wsResponse, request, response);
  151. }
  152. private void throwIfAdminOnlyParametersAreUsed(Request request) {
  153. if (!userSession.isSystemAdministrator()) {
  154. throwIfParameterValuePresent(request, LAST_CONNECTION_DATE_FROM);
  155. throwIfParameterValuePresent(request, LAST_CONNECTION_DATE_TO);
  156. throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_FROM);
  157. throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_TO);
  158. }
  159. }
  160. private Users.SearchWsResponse doHandle(UsersSearchRequest request) {
  161. SearchResults<UserSearchResult> userSearchResults = userService.findUsers(request);
  162. Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal(userSearchResults.total());
  163. return searchWsReponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging);
  164. }
  165. private UsersSearchRequest toSearchRequest(Request request) {
  166. int pageSize = request.mandatoryParamAsInt(PAGE_SIZE);
  167. checkArgument(pageSize <= MAX_PAGE_SIZE, "The '%s' parameter must be less than %s", PAGE_SIZE, MAX_PAGE_SIZE);
  168. return UsersSearchRequest.builder()
  169. .setQuery(request.param(TEXT_QUERY))
  170. .setDeactivated(request.mandatoryParamAsBoolean(DEACTIVATED_PARAM))
  171. .setManaged(request.paramAsBoolean(MANAGED_PARAM))
  172. .setLastConnectionDateFrom(request.param(LAST_CONNECTION_DATE_FROM))
  173. .setLastConnectionDateTo(request.param(LAST_CONNECTION_DATE_TO))
  174. .setSonarLintLastConnectionDateFrom(request.param(SONAR_LINT_LAST_CONNECTION_DATE_FROM))
  175. .setSonarLintLastConnectionDateTo(request.param(SONAR_LINT_LAST_CONNECTION_DATE_TO))
  176. .setPage(request.mandatoryParamAsInt(PAGE))
  177. .setPageSize(pageSize)
  178. .build();
  179. }
  180. private static void throwIfParameterValuePresent(Request request, String parameter) {
  181. Optional.ofNullable(request.param(parameter)).ifPresent(v -> throwForbiddenFor(parameter));
  182. }
  183. private static void throwForbiddenFor(String parameterName) {
  184. throw new ServerException(403, "parameter " + parameterName + " requires Administer System permission.");
  185. }
  186. }