LCOV - code coverage report
Current view: top level - server/restapi/group - ListGroups.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 140 164 85.4 %
Date: 2022-11-19 15:00:39 Functions: 30 38 78.9 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750