Line data Source code
1 : // Copyright (C) 2014 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.account; 16 : 17 : import com.google.common.base.Strings; 18 : import com.google.common.collect.ImmutableList; 19 : import com.google.gerrit.entities.Account; 20 : import com.google.gerrit.extensions.client.ListAccountsOption; 21 : import com.google.gerrit.extensions.client.ListOption; 22 : import com.google.gerrit.extensions.common.AccountInfo; 23 : import com.google.gerrit.extensions.common.AccountVisibility; 24 : import com.google.gerrit.extensions.restapi.BadRequestException; 25 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 26 : import com.google.gerrit.extensions.restapi.Response; 27 : import com.google.gerrit.extensions.restapi.RestApiException; 28 : import com.google.gerrit.extensions.restapi.RestReadView; 29 : import com.google.gerrit.extensions.restapi.TopLevelResource; 30 : import com.google.gerrit.index.query.Predicate; 31 : import com.google.gerrit.index.query.QueryParseException; 32 : import com.google.gerrit.index.query.QueryResult; 33 : import com.google.gerrit.server.account.AccountDirectory.FillOptions; 34 : import com.google.gerrit.server.account.AccountInfoComparator; 35 : import com.google.gerrit.server.account.AccountLoader; 36 : import com.google.gerrit.server.account.AccountState; 37 : import com.google.gerrit.server.config.GerritServerConfig; 38 : import com.google.gerrit.server.permissions.GlobalPermission; 39 : import com.google.gerrit.server.permissions.PermissionBackend; 40 : import com.google.gerrit.server.permissions.PermissionBackendException; 41 : import com.google.gerrit.server.query.account.AccountPredicates; 42 : import com.google.gerrit.server.query.account.AccountQueryBuilder; 43 : import com.google.gerrit.server.query.account.AccountQueryProcessor; 44 : import com.google.inject.Inject; 45 : import com.google.inject.Provider; 46 : import java.util.Collections; 47 : import java.util.EnumSet; 48 : import java.util.LinkedHashMap; 49 : import java.util.List; 50 : import java.util.Map; 51 : import java.util.Set; 52 : import org.eclipse.jgit.lib.Config; 53 : import org.kohsuke.args4j.Option; 54 : 55 : /** 56 : * REST endpoint to query accounts. 57 : * 58 : * <p>This REST endpoint handles {@code GET /accounts/} requests. 59 : * 60 : * <p>The account queries are parsed by {@link AccountQueryBuilder} and executed by {@link 61 : * AccountQueryProcessor}. 62 : */ 63 : public class QueryAccounts implements RestReadView<TopLevelResource> { 64 : private static final int MAX_SUGGEST_RESULTS = 100; 65 : 66 : private final PermissionBackend permissionBackend; 67 : private final AccountLoader.Factory accountLoaderFactory; 68 : private final AccountQueryBuilder queryBuilder; 69 : private final Provider<AccountQueryProcessor> queryProcessorProvider; 70 : private final boolean suggestConfig; 71 : private final int suggestFrom; 72 : 73 : private AccountLoader accountLoader; 74 : private boolean suggest; 75 : private Integer limit; 76 7 : private int suggestLimit = 10; 77 : private String query; 78 : private Integer start; 79 : private EnumSet<ListAccountsOption> options; 80 : 81 : @Option(name = "--suggest", metaVar = "SUGGEST", usage = "suggest users") 82 : public void setSuggest(boolean suggest) { 83 5 : this.suggest = suggest; 84 5 : } 85 : 86 : @Option( 87 : name = "--limit", 88 : aliases = {"-n"}, 89 : metaVar = "CNT", 90 : usage = "maximum number of users to return") 91 : public void setLimit(int n) { 92 5 : this.limit = n; 93 : 94 5 : if (n < 0) { 95 0 : suggestLimit = 10; 96 5 : } else if (n == 0) { 97 5 : suggestLimit = MAX_SUGGEST_RESULTS; 98 : } else { 99 2 : suggestLimit = Math.min(n, MAX_SUGGEST_RESULTS); 100 : } 101 5 : } 102 : 103 : @Option(name = "-o", usage = "Output options per account") 104 : public void addOption(ListAccountsOption o) { 105 2 : options.add(o); 106 2 : } 107 : 108 : @Option(name = "-O", usage = "Output option flags, in hex") 109 : void setOptionFlagsHex(String hex) throws BadRequestException { 110 0 : options.addAll(ListOption.fromHexString(ListAccountsOption.class, hex)); 111 0 : } 112 : 113 : @Option( 114 : name = "--query", 115 : aliases = {"-q"}, 116 : metaVar = "QUERY", 117 : usage = "match users") 118 : public void setQuery(String query) { 119 6 : this.query = query; 120 6 : } 121 : 122 : @Option( 123 : name = "--start", 124 : aliases = {"-S"}, 125 : metaVar = "CNT", 126 : usage = "Number of accounts to skip") 127 : public void setStart(int start) { 128 5 : this.start = start; 129 5 : } 130 : 131 : @Inject 132 : QueryAccounts( 133 : PermissionBackend permissionBackend, 134 : AccountLoader.Factory accountLoaderFactory, 135 : AccountQueryBuilder queryBuilder, 136 : Provider<AccountQueryProcessor> queryProcessorProvider, 137 7 : @GerritServerConfig Config cfg) { 138 7 : this.permissionBackend = permissionBackend; 139 7 : this.accountLoaderFactory = accountLoaderFactory; 140 7 : this.queryBuilder = queryBuilder; 141 7 : this.queryProcessorProvider = queryProcessorProvider; 142 7 : this.suggestFrom = cfg.getInt("suggest", null, "from", 0); 143 7 : this.options = EnumSet.noneOf(ListAccountsOption.class); 144 : 145 7 : if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) { 146 0 : suggestConfig = false; 147 : } else { 148 : boolean suggest; 149 : try { 150 7 : AccountVisibility av = cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL); 151 7 : suggest = (av != AccountVisibility.NONE); 152 0 : } catch (IllegalArgumentException err) { 153 0 : suggest = cfg.getBoolean("suggest", null, "accounts", true); 154 7 : } 155 7 : this.suggestConfig = suggest; 156 : } 157 7 : } 158 : 159 : @Override 160 : public Response<List<AccountInfo>> apply(TopLevelResource rsrc) 161 : throws RestApiException, PermissionBackendException { 162 7 : if (Strings.isNullOrEmpty(query)) { 163 1 : throw new BadRequestException("missing query field"); 164 : } 165 : 166 6 : if (suggest && (!suggestConfig || query.length() < suggestFrom)) { 167 0 : return Response.ok(Collections.emptyList()); 168 : } 169 : 170 6 : Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID); 171 6 : if (options.contains(ListAccountsOption.DETAILS)) { 172 2 : fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); 173 : } 174 6 : boolean modifyAccountCapabilityChecked = false; 175 6 : if (options.contains(ListAccountsOption.ALL_EMAILS)) { 176 2 : permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT); 177 2 : modifyAccountCapabilityChecked = true; 178 2 : fillOptions.add(FillOptions.EMAIL); 179 2 : fillOptions.add(FillOptions.SECONDARY_EMAILS); 180 : } 181 6 : if (suggest) { 182 3 : fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); 183 3 : fillOptions.add(FillOptions.EMAIL); 184 : 185 3 : if (modifyAccountCapabilityChecked) { 186 0 : fillOptions.add(FillOptions.SECONDARY_EMAILS); 187 : } else { 188 3 : if (permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) { 189 3 : fillOptions.add(FillOptions.SECONDARY_EMAILS); 190 : } 191 : } 192 : } 193 6 : accountLoader = accountLoaderFactory.create(fillOptions); 194 : 195 6 : AccountQueryProcessor queryProcessor = queryProcessorProvider.get(); 196 6 : if (queryProcessor.isDisabled()) { 197 0 : throw new MethodNotAllowedException("query disabled"); 198 : } 199 : 200 6 : if (limit != null) { 201 5 : queryProcessor.setUserProvidedLimit(limit); 202 : } 203 : 204 6 : if (start != null) { 205 5 : if (start < 0) { 206 2 : throw new BadRequestException("'start' parameter cannot be less than zero"); 207 : } 208 5 : queryProcessor.setStart(start); 209 : } 210 : 211 6 : Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>(); 212 : try { 213 : Predicate<AccountState> queryPred; 214 6 : if (suggest) { 215 3 : queryPred = queryBuilder.defaultQuery(query); 216 3 : queryProcessor.setUserProvidedLimit(suggestLimit); 217 : } else { 218 6 : queryPred = queryBuilder.parse(query); 219 : } 220 6 : if (!AccountPredicates.hasActive(queryPred)) { 221 : // if neither 'is:active' nor 'is:inactive' appears in the query only 222 : // active accounts should be queried 223 5 : queryPred = AccountPredicates.andActive(queryPred); 224 : } 225 6 : QueryResult<AccountState> result = queryProcessor.query(queryPred); 226 6 : for (AccountState accountState : result.entities()) { 227 5 : Account.Id id = accountState.account().id(); 228 5 : matches.put(id, accountLoader.get(id)); 229 5 : } 230 : 231 6 : accountLoader.fill(); 232 : 233 6 : List<AccountInfo> sorted = 234 6 : AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values()); 235 6 : if (!sorted.isEmpty() && result.more()) { 236 2 : sorted.get(sorted.size() - 1)._moreAccounts = true; 237 : } 238 6 : return Response.ok(sorted); 239 2 : } catch (QueryParseException e) { 240 2 : if (suggest) { 241 0 : return Response.ok(ImmutableList.of()); 242 : } 243 2 : throw new BadRequestException(e.getMessage(), e); 244 : } 245 : } 246 : }