Line data Source code
1 : // Copyright (C) 2013 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.group;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static java.util.stream.Collectors.toList;
20 :
21 : import com.google.common.base.MoreObjects;
22 : import com.google.common.base.Strings;
23 : import com.google.common.collect.Lists;
24 : import com.google.gerrit.common.Nullable;
25 : import com.google.gerrit.entities.Account;
26 : import com.google.gerrit.entities.AccountGroup;
27 : import com.google.gerrit.entities.GroupDescription;
28 : import com.google.gerrit.entities.GroupReference;
29 : import com.google.gerrit.exceptions.NoSuchGroupException;
30 : import com.google.gerrit.extensions.client.ListGroupsOption;
31 : import com.google.gerrit.extensions.client.ListOption;
32 : import com.google.gerrit.extensions.common.GroupInfo;
33 : import com.google.gerrit.extensions.restapi.BadRequestException;
34 : import com.google.gerrit.extensions.restapi.Response;
35 : import com.google.gerrit.extensions.restapi.RestApiException;
36 : import com.google.gerrit.extensions.restapi.RestReadView;
37 : import com.google.gerrit.extensions.restapi.TopLevelResource;
38 : import com.google.gerrit.extensions.restapi.Url;
39 : import com.google.gerrit.server.CurrentUser;
40 : import com.google.gerrit.server.IdentifiedUser;
41 : import com.google.gerrit.server.account.AccountResource;
42 : import com.google.gerrit.server.account.GroupBackend;
43 : import com.google.gerrit.server.account.GroupCache;
44 : import com.google.gerrit.server.account.GroupControl;
45 : import com.google.gerrit.server.group.GroupResolver;
46 : import com.google.gerrit.server.group.InternalGroupDescription;
47 : import com.google.gerrit.server.group.db.Groups;
48 : import com.google.gerrit.server.permissions.PermissionBackendException;
49 : import com.google.gerrit.server.project.ProjectState;
50 : import com.google.gerrit.server.restapi.account.GetGroups;
51 : import com.google.inject.Inject;
52 : import com.google.inject.Provider;
53 : import java.io.IOException;
54 : import java.util.ArrayList;
55 : import java.util.Collection;
56 : import java.util.Comparator;
57 : import java.util.EnumSet;
58 : import java.util.HashSet;
59 : import java.util.List;
60 : import java.util.Locale;
61 : import java.util.NavigableMap;
62 : import java.util.Set;
63 : import java.util.TreeMap;
64 : import java.util.function.Predicate;
65 : import java.util.regex.Pattern;
66 : import java.util.stream.Stream;
67 : import org.eclipse.jgit.errors.ConfigInvalidException;
68 : import org.kohsuke.args4j.Option;
69 :
70 : /** List groups visible to the calling user. */
71 : public class ListGroups implements RestReadView<TopLevelResource> {
72 5 : private static final Comparator<GroupDescription.Internal> GROUP_COMPARATOR =
73 5 : Comparator.comparing(GroupDescription.Basic::getName);
74 :
75 : protected final GroupCache groupCache;
76 :
77 5 : private final List<ProjectState> projects = new ArrayList<>();
78 5 : private final Set<AccountGroup.UUID> groupsToInspect = new HashSet<>();
79 : private final GroupControl.Factory groupControlFactory;
80 : private final GroupControl.GenericFactory genericGroupControlFactory;
81 : private final Provider<IdentifiedUser> identifiedUser;
82 : private final IdentifiedUser.GenericFactory userFactory;
83 : private final GetGroups accountGetGroups;
84 : private final GroupJson json;
85 : private final GroupBackend groupBackend;
86 : private final Groups groups;
87 : private final GroupResolver groupResolver;
88 :
89 5 : private Set<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class);
90 : private boolean visibleToAll;
91 : private Account.Id user;
92 : private boolean owned;
93 : private int limit;
94 : private int start;
95 : private String matchSubstring;
96 : private String matchRegex;
97 : private String suggest;
98 : private String ownedBy;
99 :
100 : @Option(
101 : name = "--project",
102 : aliases = {"-p"},
103 : usage = "projects for which the groups should be listed")
104 : public void addProject(ProjectState project) {
105 0 : projects.add(project);
106 0 : }
107 :
108 : @Option(
109 : name = "--visible-to-all",
110 : usage = "to list only groups that are visible to all registered users")
111 : public void setVisibleToAll(boolean visibleToAll) {
112 1 : this.visibleToAll = visibleToAll;
113 1 : }
114 :
115 : @Option(
116 : name = "--user",
117 : aliases = {"-u"},
118 : usage = "user for which the groups should be listed")
119 : public void setUser(Account.Id user) {
120 1 : this.user = user;
121 1 : }
122 :
123 : @Option(
124 : name = "--owned",
125 : usage =
126 : "to list only groups that are owned by the"
127 : + " specified user or by the calling user if no user was specifed")
128 : public void setOwned(boolean owned) {
129 1 : this.owned = owned;
130 1 : }
131 :
132 : @Option(
133 : name = "--group",
134 : aliases = {"-g"},
135 : usage = "group to inspect")
136 : public void addGroup(AccountGroup.UUID uuid) {
137 1 : groupsToInspect.add(uuid);
138 1 : }
139 :
140 : @Option(
141 : name = "--limit",
142 : aliases = {"-n"},
143 : metaVar = "CNT",
144 : usage = "maximum number of groups to list")
145 : public void setLimit(int limit) {
146 1 : this.limit = limit;
147 1 : }
148 :
149 : @Option(
150 : name = "--start",
151 : aliases = {"-S"},
152 : metaVar = "CNT",
153 : usage = "number of groups to skip")
154 : public void setStart(int start) {
155 1 : this.start = start;
156 1 : }
157 :
158 : @Option(
159 : name = "--match",
160 : aliases = {"-m"},
161 : metaVar = "MATCH",
162 : usage = "match group substring")
163 : public void setMatchSubstring(String matchSubstring) {
164 1 : this.matchSubstring = matchSubstring;
165 1 : }
166 :
167 : @Option(
168 : name = "--regex",
169 : aliases = {"-r"},
170 : metaVar = "REGEX",
171 : usage = "match group regex")
172 : public void setMatchRegex(String matchRegex) {
173 1 : this.matchRegex = matchRegex;
174 1 : }
175 :
176 : @Option(
177 : name = "--suggest",
178 : aliases = {"-s"},
179 : usage = "to get a suggestion of groups")
180 : public void setSuggest(String suggest) {
181 1 : this.suggest = suggest;
182 1 : }
183 :
184 : @Option(name = "-o", usage = "Output options per group")
185 : void addOption(ListGroupsOption o) {
186 0 : options.add(o);
187 0 : }
188 :
189 : @Option(name = "-O", usage = "Output option flags, in hex")
190 : void setOptionFlagsHex(String hex) throws BadRequestException {
191 0 : options.addAll(ListOption.fromHexString(ListGroupsOption.class, hex));
192 0 : }
193 :
194 : @Option(
195 : name = "--owned-by",
196 : aliases = {"--ownedby"},
197 : usage = "list groups owned by the given group uuid")
198 : public void setOwnedBy(String ownedBy) {
199 1 : this.ownedBy = ownedBy;
200 1 : }
201 :
202 : @Inject
203 : protected ListGroups(
204 : final GroupCache groupCache,
205 : final GroupControl.Factory groupControlFactory,
206 : final GroupControl.GenericFactory genericGroupControlFactory,
207 : final Provider<IdentifiedUser> identifiedUser,
208 : final IdentifiedUser.GenericFactory userFactory,
209 : final GetGroups accountGetGroups,
210 : final GroupResolver groupResolver,
211 : GroupJson json,
212 : GroupBackend groupBackend,
213 5 : Groups groups) {
214 5 : this.groupCache = groupCache;
215 5 : this.groupControlFactory = groupControlFactory;
216 5 : this.genericGroupControlFactory = genericGroupControlFactory;
217 5 : this.identifiedUser = identifiedUser;
218 5 : this.userFactory = userFactory;
219 5 : this.accountGetGroups = accountGetGroups;
220 5 : this.json = json;
221 5 : this.groupBackend = groupBackend;
222 5 : this.groups = groups;
223 5 : this.groupResolver = groupResolver;
224 5 : }
225 :
226 : public void setOptions(Set<ListGroupsOption> options) {
227 1 : this.options = options;
228 1 : }
229 :
230 : public Account.Id getUser() {
231 0 : return user;
232 : }
233 :
234 : public List<ProjectState> getProjects() {
235 0 : return projects;
236 : }
237 :
238 : @Override
239 : public Response<NavigableMap<String, GroupInfo>> apply(TopLevelResource resource)
240 : throws Exception {
241 4 : NavigableMap<String, GroupInfo> output = new TreeMap<>();
242 4 : for (GroupInfo info : get()) {
243 4 : output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
244 4 : info.name = null;
245 4 : }
246 4 : return Response.ok(output);
247 : }
248 :
249 : public List<GroupInfo> get() throws Exception {
250 4 : if (!Strings.isNullOrEmpty(suggest)) {
251 1 : return suggestGroups();
252 : }
253 :
254 4 : if (!Strings.isNullOrEmpty(matchSubstring) && !Strings.isNullOrEmpty(matchRegex)) {
255 1 : throw new BadRequestException("Specify one of m/r");
256 : }
257 :
258 4 : if (ownedBy != null) {
259 1 : return getGroupsOwnedBy(ownedBy);
260 : }
261 :
262 4 : if (owned) {
263 0 : return getGroupsOwnedBy(user != null ? userFactory.create(user) : identifiedUser.get());
264 : }
265 :
266 4 : if (user != null) {
267 1 : return accountGetGroups.apply(new AccountResource(userFactory.create(user))).value();
268 : }
269 :
270 4 : return getAllGroups();
271 : }
272 :
273 : private List<GroupInfo> getAllGroups()
274 : throws IOException, ConfigInvalidException, PermissionBackendException {
275 4 : Pattern pattern = getRegexPattern();
276 4 : Stream<GroupDescription.Internal> existingGroups =
277 4 : loadGroups(
278 4 : getAllExistingGroups()
279 4 : .filter(group -> isRelevant(pattern, group))
280 4 : .map(g -> g.getUUID())
281 4 : .collect(toImmutableSet()))
282 4 : .stream()
283 4 : .filter(this::isVisible)
284 4 : .sorted(GROUP_COMPARATOR)
285 4 : .skip(start);
286 4 : if (limit > 0) {
287 0 : existingGroups = existingGroups.limit(limit);
288 : }
289 4 : List<GroupDescription.Internal> relevantGroups = existingGroups.collect(toImmutableList());
290 4 : List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(relevantGroups.size());
291 4 : for (GroupDescription.Internal group : relevantGroups) {
292 4 : groupInfos.add(json.addOptions(options).format(group));
293 4 : }
294 4 : return groupInfos;
295 : }
296 :
297 : private Stream<GroupReference> getAllExistingGroups() throws IOException, ConfigInvalidException {
298 4 : if (!projects.isEmpty()) {
299 0 : return projects.stream()
300 0 : .map(ProjectState::getAllGroups)
301 0 : .flatMap(Collection::stream)
302 0 : .distinct();
303 : }
304 4 : return groups.getAllGroupReferences();
305 : }
306 :
307 : private List<GroupInfo> suggestGroups() throws BadRequestException, PermissionBackendException {
308 1 : if (conflictingSuggestParameters()) {
309 1 : throw new BadRequestException(
310 : "You should only have no more than one --project and -n with --suggest");
311 : }
312 1 : List<GroupReference> groupRefs =
313 1 : groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)).stream()
314 1 : .limit(limit <= 0 ? 10 : Math.min(limit, 10))
315 1 : .collect(toList());
316 :
317 1 : List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
318 1 : for (GroupReference ref : groupRefs) {
319 1 : GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
320 1 : if (desc != null) {
321 1 : groupInfos.add(json.addOptions(options).format(desc));
322 : }
323 1 : }
324 1 : return groupInfos;
325 : }
326 :
327 : private boolean conflictingSuggestParameters() {
328 1 : if (Strings.isNullOrEmpty(suggest)) {
329 0 : return false;
330 : }
331 1 : if (projects.size() > 1) {
332 0 : return true;
333 : }
334 1 : if (visibleToAll) {
335 1 : return true;
336 : }
337 1 : if (user != null) {
338 1 : return true;
339 : }
340 1 : if (owned) {
341 1 : return true;
342 : }
343 1 : if (ownedBy != null) {
344 0 : return true;
345 : }
346 1 : if (start != 0) {
347 1 : return true;
348 : }
349 1 : if (!groupsToInspect.isEmpty()) {
350 0 : return true;
351 : }
352 1 : if (!Strings.isNullOrEmpty(matchSubstring)) {
353 1 : return true;
354 : }
355 1 : if (!Strings.isNullOrEmpty(matchRegex)) {
356 1 : return true;
357 : }
358 1 : return false;
359 : }
360 :
361 : private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
362 : throws IOException, ConfigInvalidException, PermissionBackendException {
363 1 : Pattern pattern = getRegexPattern();
364 1 : Stream<? extends GroupDescription.Internal> foundGroups =
365 1 : loadGroups(
366 : groups
367 1 : .getAllGroupReferences()
368 1 : .filter(group -> isRelevant(pattern, group))
369 1 : .map(g -> g.getUUID())
370 1 : .collect(toImmutableSet()))
371 1 : .stream()
372 1 : .filter(this::isVisible)
373 1 : .filter(filter)
374 1 : .sorted(GROUP_COMPARATOR)
375 1 : .skip(start);
376 1 : if (limit > 0) {
377 0 : foundGroups = foundGroups.limit(limit);
378 : }
379 1 : List<GroupDescription.Internal> ownedGroups = foundGroups.collect(toImmutableList());
380 1 : List<GroupInfo> groupInfos = new ArrayList<>(ownedGroups.size());
381 1 : for (GroupDescription.Internal group : ownedGroups) {
382 1 : groupInfos.add(json.addOptions(options).format(group));
383 1 : }
384 1 : return groupInfos;
385 : }
386 :
387 : private Set<GroupDescription.Internal> loadGroups(Collection<AccountGroup.UUID> groupUuids) {
388 4 : return groupCache.get(groupUuids).values().stream()
389 4 : .map(InternalGroupDescription::new)
390 4 : .collect(toImmutableSet());
391 : }
392 :
393 : private List<GroupInfo> getGroupsOwnedBy(String id)
394 : throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
395 1 : String uuid = groupResolver.parse(id).getGroupUUID().get();
396 1 : return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
397 : }
398 :
399 : private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
400 : throws IOException, ConfigInvalidException, PermissionBackendException {
401 0 : return filterGroupsOwnedBy(group -> isOwner(user, group));
402 : }
403 :
404 : private boolean isOwner(CurrentUser user, GroupDescription.Internal group) {
405 : try {
406 0 : return genericGroupControlFactory.controlFor(user, group.getGroupUUID()).isOwner();
407 0 : } catch (NoSuchGroupException e) {
408 0 : return false;
409 : }
410 : }
411 :
412 : @Nullable
413 : private Pattern getRegexPattern() {
414 4 : return Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
415 : }
416 :
417 : private boolean isRelevant(Pattern pattern, GroupReference group) {
418 4 : if (!Strings.isNullOrEmpty(matchSubstring)) {
419 1 : if (!group.getName().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US))) {
420 1 : return false;
421 : }
422 4 : } else if (pattern != null) {
423 1 : if (!pattern.matcher(group.getName()).matches()) {
424 1 : return false;
425 : }
426 : }
427 4 : return groupsToInspect.isEmpty() || groupsToInspect.contains(group.getUUID());
428 : }
429 :
430 : private boolean isVisible(GroupDescription.Internal group) {
431 4 : if (visibleToAll && !group.isVisibleToAll()) {
432 0 : return false;
433 : }
434 4 : GroupControl c = groupControlFactory.controlFor(group);
435 4 : return c.isVisible();
436 : }
437 : }
|