Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

SearchAction.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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 com.google.common.collect.Multimap;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.function.Function;
  28. import javax.annotation.CheckForNull;
  29. import javax.annotation.Nullable;
  30. import org.sonar.api.server.ws.Change;
  31. import org.sonar.api.server.ws.Request;
  32. import org.sonar.api.server.ws.Response;
  33. import org.sonar.api.server.ws.WebService;
  34. import org.sonar.api.utils.Paging;
  35. import org.sonar.db.DbClient;
  36. import org.sonar.db.DbSession;
  37. import org.sonar.db.user.UserDto;
  38. import org.sonar.server.es.SearchOptions;
  39. import org.sonar.server.es.SearchResult;
  40. import org.sonar.server.issue.ws.AvatarResolver;
  41. import org.sonar.server.user.UserSession;
  42. import org.sonar.server.user.index.UserDoc;
  43. import org.sonar.server.user.index.UserIndex;
  44. import org.sonar.server.user.index.UserQuery;
  45. import org.sonarqube.ws.Users;
  46. import org.sonarqube.ws.Users.SearchWsResponse;
  47. import static com.google.common.base.MoreObjects.firstNonNull;
  48. import static com.google.common.base.Preconditions.checkArgument;
  49. import static com.google.common.base.Strings.emptyToNull;
  50. import static java.util.Optional.ofNullable;
  51. import static org.sonar.api.server.ws.WebService.Param.FIELDS;
  52. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  53. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  54. import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
  55. import static org.sonar.api.utils.DateUtils.formatDateTime;
  56. import static org.sonar.api.utils.Paging.forPageIndex;
  57. import static org.sonar.core.util.stream.MoreCollectors.toList;
  58. import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
  59. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_ACTIVE;
  60. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_AVATAR;
  61. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EMAIL;
  62. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EXTERNAL_IDENTITY;
  63. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EXTERNAL_PROVIDER;
  64. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_GROUPS;
  65. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_LOCAL;
  66. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_NAME;
  67. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_SCM_ACCOUNTS;
  68. import static org.sonar.server.user.ws.UserJsonWriter.FIELD_TOKENS_COUNT;
  69. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  70. import static org.sonarqube.ws.Users.SearchWsResponse.Groups;
  71. import static org.sonarqube.ws.Users.SearchWsResponse.ScmAccounts;
  72. import static org.sonarqube.ws.Users.SearchWsResponse.User;
  73. import static org.sonarqube.ws.Users.SearchWsResponse.newBuilder;
  74. public class SearchAction implements UsersWsAction {
  75. private static final int MAX_PAGE_SIZE = 500;
  76. private final UserSession userSession;
  77. private final UserIndex userIndex;
  78. private final DbClient dbClient;
  79. private final AvatarResolver avatarResolver;
  80. public SearchAction(UserSession userSession, UserIndex userIndex, DbClient dbClient, AvatarResolver avatarResolver) {
  81. this.userSession = userSession;
  82. this.userIndex = userIndex;
  83. this.dbClient = dbClient;
  84. this.avatarResolver = avatarResolver;
  85. }
  86. @Override
  87. public void define(WebService.NewController controller) {
  88. WebService.NewAction action = controller.createAction("search")
  89. .setDescription("Get a list of active users. <br/>" +
  90. "The following fields are only returned when user has Administer System permission or for logged-in in user :" +
  91. "<ul>" +
  92. " <li>'email'</li>" +
  93. " <li>'externalIdentity'</li>" +
  94. " <li>'externalProvider'</li>" +
  95. " <li>'groups'</li>" +
  96. " <li>'lastConnectionDate'</li>" +
  97. " <li>'tokensCount'</li>" +
  98. "</ul>" +
  99. "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.")
  100. .setSince("3.6")
  101. .setChangelog(
  102. new Change("7.7", "New field 'lastConnectionDate' is added to response"),
  103. new Change("7.4", "External identity is only returned to system administrators"),
  104. new Change("6.4", "Paging response fields moved to a Paging object"),
  105. new Change("6.4", "Avatar has been added to the response"),
  106. new Change("6.4", "Email is only returned when user has Administer System permission"))
  107. .setHandler(this)
  108. .setResponseExample(getClass().getResource("search-example.json"));
  109. action.createFieldsParam(UserJsonWriter.FIELDS)
  110. .setDeprecatedSince("5.4");
  111. action.addPagingParams(50, MAX_LIMIT);
  112. action.createParam(TEXT_QUERY)
  113. .setMinimumLength(2)
  114. .setDescription("Filter on login, name and email");
  115. }
  116. @Override
  117. public void handle(Request request, Response response) throws Exception {
  118. Users.SearchWsResponse wsResponse = doHandle(toSearchRequest(request));
  119. writeProtobuf(wsResponse, request, response);
  120. }
  121. private Users.SearchWsResponse doHandle(SearchRequest request) {
  122. SearchOptions options = new SearchOptions().setPage(request.getPage(), request.getPageSize());
  123. List<String> fields = request.getPossibleFields();
  124. SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setTextQuery(request.getQuery()).build(), options);
  125. try (DbSession dbSession = dbClient.openSession(false)) {
  126. List<String> logins = result.getDocs().stream().map(UserDoc::login).collect(toList());
  127. Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
  128. List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
  129. Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByUsers(dbSession, users);
  130. Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal((int) result.getTotal());
  131. return buildResponse(users, groupsByLogin, tokenCountsByLogin, fields, paging);
  132. }
  133. }
  134. private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin,
  135. @Nullable List<String> fields, Paging paging) {
  136. SearchWsResponse.Builder responseBuilder = newBuilder();
  137. users.forEach(user -> responseBuilder.addUsers(towsUser(user, firstNonNull(tokenCountsByLogin.get(user.getUuid()), 0), groupsByLogin.get(user.getLogin()), fields)));
  138. responseBuilder.getPagingBuilder()
  139. .setPageIndex(paging.pageIndex())
  140. .setPageSize(paging.pageSize())
  141. .setTotal(paging.total())
  142. .build();
  143. return responseBuilder.build();
  144. }
  145. private User towsUser(UserDto user, @Nullable Integer tokensCount, Collection<String> groups, @Nullable Collection<String> fields) {
  146. User.Builder userBuilder = User.newBuilder()
  147. .setLogin(user.getLogin());
  148. setIfNeeded(FIELD_NAME, fields, user.getName(), userBuilder::setName);
  149. if (userSession.isLoggedIn()) {
  150. setIfNeeded(FIELD_AVATAR, fields, emptyToNull(user.getEmail()), u -> userBuilder.setAvatar(avatarResolver.create(user)));
  151. setIfNeeded(FIELD_ACTIVE, fields, user.isActive(), userBuilder::setActive);
  152. setIfNeeded(FIELD_LOCAL, fields, user.isLocal(), userBuilder::setLocal);
  153. setIfNeeded(FIELD_EXTERNAL_PROVIDER, fields, user.getExternalIdentityProvider(), userBuilder::setExternalProvider);
  154. setIfNeeded(isNeeded(FIELD_SCM_ACCOUNTS, fields) && !user.getScmAccountsAsList().isEmpty(), user.getScmAccountsAsList(),
  155. scm -> userBuilder.setScmAccounts(ScmAccounts.newBuilder().addAllScmAccounts(scm)));
  156. }
  157. if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), user.getUuid())) {
  158. setIfNeeded(FIELD_EMAIL, fields, user.getEmail(), userBuilder::setEmail);
  159. setIfNeeded(isNeeded(FIELD_GROUPS, fields) && !groups.isEmpty(), groups,
  160. g -> userBuilder.setGroups(Groups.newBuilder().addAllGroups(g)));
  161. setIfNeeded(FIELD_EXTERNAL_IDENTITY, fields, user.getExternalLogin(), userBuilder::setExternalIdentity);
  162. setIfNeeded(FIELD_TOKENS_COUNT, fields, tokensCount, userBuilder::setTokensCount);
  163. ofNullable(user.getLastConnectionDate()).ifPresent(date -> userBuilder.setLastConnectionDate(formatDateTime(date)));
  164. }
  165. return userBuilder.build();
  166. }
  167. private static <PARAM> void setIfNeeded(String field, @Nullable Collection<String> fields, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
  168. setIfNeeded(isNeeded(field, fields), parameter, setter);
  169. }
  170. private static <PARAM> void setIfNeeded(boolean condition, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
  171. if (parameter != null && condition) {
  172. setter.apply(parameter);
  173. }
  174. }
  175. private static boolean isNeeded(String field, @Nullable Collection<String> fields) {
  176. return fields == null || fields.isEmpty() || fields.contains(field);
  177. }
  178. private static SearchRequest toSearchRequest(Request request) {
  179. int pageSize = request.mandatoryParamAsInt(PAGE_SIZE);
  180. checkArgument(pageSize <= MAX_PAGE_SIZE, "The '%s' parameter must be less than %s", PAGE_SIZE, MAX_PAGE_SIZE);
  181. return SearchRequest.builder()
  182. .setQuery(request.param(TEXT_QUERY))
  183. .setPage(request.mandatoryParamAsInt(PAGE))
  184. .setPageSize(pageSize)
  185. .setPossibleFields(request.paramAsStrings(FIELDS))
  186. .build();
  187. }
  188. private static class SearchRequest {
  189. private final Integer page;
  190. private final Integer pageSize;
  191. private final String query;
  192. private final List<String> possibleFields;
  193. private SearchRequest(Builder builder) {
  194. this.page = builder.page;
  195. this.pageSize = builder.pageSize;
  196. this.query = builder.query;
  197. this.possibleFields = builder.additionalFields;
  198. }
  199. @CheckForNull
  200. public Integer getPage() {
  201. return page;
  202. }
  203. @CheckForNull
  204. public Integer getPageSize() {
  205. return pageSize;
  206. }
  207. @CheckForNull
  208. public String getQuery() {
  209. return query;
  210. }
  211. public List<String> getPossibleFields() {
  212. return possibleFields;
  213. }
  214. public static Builder builder() {
  215. return new Builder();
  216. }
  217. }
  218. private static class Builder {
  219. private Integer page;
  220. private Integer pageSize;
  221. private String query;
  222. private List<String> additionalFields = new ArrayList<>();
  223. private Builder() {
  224. // enforce factory method use
  225. }
  226. public Builder setPage(@Nullable Integer page) {
  227. this.page = page;
  228. return this;
  229. }
  230. public Builder setPageSize(@Nullable Integer pageSize) {
  231. this.pageSize = pageSize;
  232. return this;
  233. }
  234. public Builder setQuery(@Nullable String query) {
  235. this.query = query;
  236. return this;
  237. }
  238. public Builder setPossibleFields(List<String> possibleFields) {
  239. this.additionalFields = possibleFields;
  240. return this;
  241. }
  242. public SearchRequest build() {
  243. return new SearchRequest(this);
  244. }
  245. }
  246. }