LCOV - code coverage report
Current view: top level - server/account - AccountResolver.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 184 188 97.9 %
Date: 2022-11-19 15:00:39 Functions: 85 87 97.7 %

          Line data    Source code
       1             : // Copyright (C) 2019 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.account;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      20             : import static java.util.Comparator.comparing;
      21             : import static java.util.Objects.requireNonNull;
      22             : import static java.util.stream.Collectors.joining;
      23             : 
      24             : import com.google.common.annotations.VisibleForTesting;
      25             : import com.google.common.base.Suppliers;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.collect.ImmutableSet;
      28             : import com.google.common.collect.Streams;
      29             : import com.google.gerrit.entities.Account;
      30             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      31             : import com.google.gerrit.index.Schema;
      32             : import com.google.gerrit.server.CurrentUser;
      33             : import com.google.gerrit.server.IdentifiedUser;
      34             : import com.google.gerrit.server.account.externalids.ExternalId;
      35             : import com.google.gerrit.server.config.AnonymousCowardName;
      36             : import com.google.gerrit.server.permissions.GlobalPermission;
      37             : import com.google.gerrit.server.permissions.PermissionBackend;
      38             : import com.google.gerrit.server.permissions.PermissionBackendException;
      39             : import com.google.gerrit.server.query.account.InternalAccountQuery;
      40             : import com.google.inject.Inject;
      41             : import com.google.inject.Provider;
      42             : import com.google.inject.Singleton;
      43             : import java.io.IOException;
      44             : import java.util.ArrayList;
      45             : import java.util.List;
      46             : import java.util.Optional;
      47             : import java.util.Set;
      48             : import java.util.TreeSet;
      49             : import java.util.function.Predicate;
      50             : import java.util.function.Supplier;
      51             : import java.util.regex.Matcher;
      52             : import java.util.regex.Pattern;
      53             : import java.util.stream.Stream;
      54             : import org.eclipse.jgit.errors.ConfigInvalidException;
      55             : 
      56             : /**
      57             :  * Helper for resolving accounts given arbitrary user-provided input.
      58             :  *
      59             :  * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution.
      60             :  * The algorithm for resolving accounts from a list of formats is as follows:
      61             :  *
      62             :  * <ol>
      63             :  *   <li>For each recognized format in the order listed in the method Javadoc, check whether the
      64             :  *       input matches that format.
      65             :  *   <li>If so, resolve accounts according to that format.
      66             :  *   <li>Filter out invisible and inactive accounts.
      67             :  *   <li>If the result list is non-empty, return.
      68             :  *   <li>If the format is listed above as being short-circuiting, return.
      69             :  *   <li>Otherwise, return to step 1 with the next format.
      70             :  * </ol>
      71             :  *
      72             :  * <p>The result never includes accounts that are not visible to the calling user. It also never
      73             :  * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
      74             :  */
      75             : @Singleton
      76             : public class AccountResolver {
      77             :   public static class UnresolvableAccountException extends UnprocessableEntityException {
      78             :     private static final long serialVersionUID = 1L;
      79             :     private final Result result;
      80             : 
      81             :     @VisibleForTesting
      82             :     UnresolvableAccountException(Result result) {
      83          24 :       super(exceptionMessage(result));
      84          24 :       this.result = result;
      85          24 :     }
      86             : 
      87             :     public boolean isSelf() {
      88          22 :       return result.isSelf();
      89             :     }
      90             :   }
      91             : 
      92             :   public static String exceptionMessage(Result result) {
      93          24 :     checkArgument(result.asList().size() != 1);
      94          24 :     if (result.asList().isEmpty()) {
      95          24 :       if (result.isSelf()) {
      96           7 :         return "Resolving account '" + result.input() + "' requires login";
      97             :       }
      98          24 :       if (result.filteredInactive().isEmpty()) {
      99          23 :         return "Account '" + result.input() + "' not found";
     100             :       }
     101           4 :       return result.filteredInactive().stream()
     102           4 :           .map(a -> formatForException(result, a))
     103           4 :           .collect(
     104           4 :               joining(
     105             :                   "\n",
     106             :                   "Account '"
     107           4 :                       + result.input()
     108             :                       + "' only matches inactive accounts. To use an inactive account, retry with"
     109             :                       + " one of the following exact account IDs:\n",
     110             :                   ""));
     111             :     }
     112             : 
     113           2 :     return result.asList().stream()
     114           2 :         .map(a -> formatForException(result, a))
     115           2 :         .limit(3)
     116           2 :         .collect(
     117           2 :             joining(
     118           2 :                 "\n", "Account '" + result.input() + "' is ambiguous (at most 3 shown):\n", ""));
     119             :   }
     120             : 
     121             :   private static String formatForException(Result result, AccountState state) {
     122           4 :     return state.account().id()
     123             :         + ": "
     124           4 :         + state.account().getNameEmail(result.accountResolver().anonymousCowardName);
     125             :   }
     126             : 
     127             :   public static boolean isSelf(String input) {
     128          61 :     return "self".equals(input) || "me".equals(input);
     129             :   }
     130             : 
     131             :   public class Result {
     132             :     private final String input;
     133             :     private final ImmutableList<AccountState> list;
     134             :     private final ImmutableList<AccountState> filteredInactive;
     135             : 
     136             :     @VisibleForTesting
     137          71 :     Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
     138          71 :       this.input = requireNonNull(input);
     139          71 :       this.list = canonicalize(list);
     140          71 :       this.filteredInactive = canonicalize(filteredInactive);
     141          71 :     }
     142             : 
     143             :     private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
     144          71 :       TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.account().id().get()));
     145          71 :       set.addAll(requireNonNull(list));
     146          71 :       return ImmutableList.copyOf(set);
     147             :     }
     148             : 
     149             :     public String input() {
     150          24 :       return input;
     151             :     }
     152             : 
     153             :     public boolean isSelf() {
     154          61 :       return AccountResolver.isSelf(input);
     155             :     }
     156             : 
     157             :     public ImmutableList<AccountState> asList() {
     158          24 :       return list;
     159             :     }
     160             : 
     161             :     public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException {
     162          17 :       if (list.isEmpty()) {
     163          10 :         throw new UnresolvableAccountException(this);
     164             :       }
     165          13 :       return asIdSet();
     166             :     }
     167             : 
     168             :     public ImmutableSet<Account.Id> asIdSet() {
     169          27 :       return list.stream().map(a -> a.account().id()).collect(toImmutableSet());
     170             :     }
     171             : 
     172             :     public AccountState asUnique() throws UnresolvableAccountException {
     173          61 :       ensureUnique();
     174          61 :       return list.get(0);
     175             :     }
     176             : 
     177             :     private void ensureUnique() throws UnresolvableAccountException {
     178          64 :       if (list.size() != 1) {
     179          22 :         throw new UnresolvableAccountException(this);
     180             :       }
     181          63 :     }
     182             : 
     183             :     public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
     184          56 :       ensureUnique();
     185          56 :       if (isSelf()) {
     186             :         // In the special case of "self", use the exact IdentifiedUser from the request context, to
     187             :         // preserve the peer address and any other per-request state.
     188           7 :         return self.get().asIdentifiedUser();
     189             :       }
     190          54 :       return userFactory.create(asUnique());
     191             :     }
     192             : 
     193             :     public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
     194             :         throws UnresolvableAccountException {
     195           3 :       ensureUnique();
     196           3 :       if (isSelf()) {
     197             :         // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
     198           0 :         return self.get().asIdentifiedUser();
     199             :       }
     200           3 :       return userFactory.runAs(
     201           3 :           null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
     202             :     }
     203             : 
     204             :     @VisibleForTesting
     205             :     ImmutableList<AccountState> filteredInactive() {
     206          24 :       return filteredInactive;
     207             :     }
     208             : 
     209             :     private AccountResolver accountResolver() {
     210           4 :       return AccountResolver.this;
     211             :     }
     212             :   }
     213             : 
     214             :   @VisibleForTesting
     215             :   interface Searcher<I> {
     216             :     default boolean callerShouldFilterOutInactiveCandidates() {
     217          57 :       return true;
     218             :     }
     219             : 
     220             :     default boolean callerMayAssumeCandidatesAreVisible() {
     221          62 :       return false;
     222             :     }
     223             : 
     224             :     Optional<I> tryParse(String input) throws IOException;
     225             : 
     226             :     Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
     227             : 
     228             :     boolean shortCircuitIfNoResults();
     229             : 
     230             :     default Optional<Stream<AccountState>> trySearch(String input)
     231             :         throws IOException, ConfigInvalidException {
     232          71 :       Optional<I> parsed = tryParse(input);
     233          71 :       return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
     234             :     }
     235             :   }
     236             : 
     237             :   @VisibleForTesting
     238         150 :   abstract static class StringSearcher implements Searcher<String> {
     239             :     @Override
     240             :     public final Optional<String> tryParse(String input) {
     241          71 :       return matches(input) ? Optional.of(input) : Optional.empty();
     242             :     }
     243             : 
     244             :     protected abstract boolean matches(String input);
     245             :   }
     246             : 
     247         150 :   private abstract class AccountIdSearcher implements Searcher<Account.Id> {
     248             :     @Override
     249             :     public final Stream<AccountState> search(Account.Id input) {
     250          44 :       return Streams.stream(accountCache.get(input));
     251             :     }
     252             :   }
     253             : 
     254         150 :   private class BySelf extends StringSearcher {
     255             :     @Override
     256             :     public boolean callerShouldFilterOutInactiveCandidates() {
     257          13 :       return false;
     258             :     }
     259             : 
     260             :     @Override
     261             :     public boolean callerMayAssumeCandidatesAreVisible() {
     262          13 :       return true;
     263             :     }
     264             : 
     265             :     @Override
     266             :     protected boolean matches(String input) {
     267          70 :       return "self".equals(input) || "me".equals(input);
     268             :     }
     269             : 
     270             :     @Override
     271             :     public Stream<AccountState> search(String input) {
     272          13 :       CurrentUser user = self.get();
     273          13 :       if (!user.isIdentifiedUser()) {
     274           6 :         return Stream.empty();
     275             :       }
     276          13 :       return Stream.of(user.asIdentifiedUser().state());
     277             :     }
     278             : 
     279             :     @Override
     280             :     public boolean shortCircuitIfNoResults() {
     281           6 :       return true;
     282             :     }
     283             :   }
     284             : 
     285         150 :   private class ByExactAccountId extends AccountIdSearcher {
     286             :     @Override
     287             :     public boolean callerShouldFilterOutInactiveCandidates() {
     288          44 :       return false;
     289             :     }
     290             : 
     291             :     @Override
     292             :     public Optional<Account.Id> tryParse(String input) {
     293          68 :       return Account.Id.tryParse(input);
     294             :     }
     295             : 
     296             :     @Override
     297             :     public boolean shortCircuitIfNoResults() {
     298          11 :       return true;
     299             :     }
     300             :   }
     301             : 
     302         150 :   private class ByParenthesizedAccountId extends AccountIdSearcher {
     303         150 :     private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$");
     304             : 
     305             :     @Override
     306             :     public Optional<Account.Id> tryParse(String input) {
     307          57 :       Matcher m = pattern.matcher(input);
     308          57 :       return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty();
     309             :     }
     310             : 
     311             :     @Override
     312             :     public boolean shortCircuitIfNoResults() {
     313           1 :       return true;
     314             :     }
     315             :   }
     316             : 
     317         150 :   private class ByUsername extends StringSearcher {
     318             :     @Override
     319             :     public boolean matches(String input) {
     320          57 :       return ExternalId.isValidUsername(input);
     321             :     }
     322             : 
     323             :     @Override
     324             :     public Stream<AccountState> search(String input) {
     325          51 :       return Streams.stream(accountCache.getByUsername(input));
     326             :     }
     327             : 
     328             :     @Override
     329             :     public boolean shortCircuitIfNoResults() {
     330          45 :       return false;
     331             :     }
     332             :   }
     333             : 
     334         150 :   private class ByNameAndEmail extends StringSearcher {
     335             :     @Override
     336             :     protected boolean matches(String input) {
     337          51 :       int lt = input.indexOf('<');
     338          51 :       int gt = input.indexOf('>');
     339          51 :       return lt >= 0 && gt > lt && input.contains("@");
     340             :     }
     341             : 
     342             :     @Override
     343             :     public Stream<AccountState> search(String nameOrEmail) throws IOException {
     344             :       // TODO(dborowitz): This would probably work as a Searcher<Address>
     345          12 :       int lt = nameOrEmail.indexOf('<');
     346          12 :       int gt = nameOrEmail.indexOf('>');
     347          12 :       Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
     348          12 :       ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
     349          12 :       if (allMatches.isEmpty() || allMatches.size() == 1) {
     350          11 :         return allMatches.stream();
     351             :       }
     352             : 
     353             :       // More than one match. If there are any that match the full name as well, return only that
     354             :       // subset. Otherwise, all are equally non-matching, so return the full set.
     355           2 :       if (lt == 0) {
     356             :         // No name was specified in the input string.
     357           1 :         return allMatches.stream();
     358             :       }
     359           1 :       String name = nameOrEmail.substring(0, lt - 1);
     360           1 :       ImmutableList<AccountState> nameMatches =
     361           1 :           allMatches.stream()
     362           1 :               .filter(a -> name.equals(a.account().fullName()))
     363           1 :               .collect(toImmutableList());
     364           1 :       return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
     365             :     }
     366             : 
     367             :     @Override
     368             :     public boolean shortCircuitIfNoResults() {
     369           9 :       return true;
     370             :     }
     371             :   }
     372             : 
     373         150 :   private class ByEmail extends StringSearcher {
     374             :     @Override
     375             :     protected boolean matches(String input) {
     376          51 :       return input.contains("@");
     377             :     }
     378             : 
     379             :     @Override
     380             :     public Stream<AccountState> search(String input) throws IOException {
     381          42 :       return toAccountStates(emails.getAccountFor(input));
     382             :     }
     383             : 
     384             :     @Override
     385             :     public boolean shortCircuitIfNoResults() {
     386          13 :       return true;
     387             :     }
     388             :   }
     389             : 
     390         150 :   private class FromRealm extends AccountIdSearcher {
     391             :     @Override
     392             :     public Optional<Account.Id> tryParse(String input) throws IOException {
     393          33 :       return Optional.ofNullable(realm.lookup(input));
     394             :     }
     395             : 
     396             :     @Override
     397             :     public boolean shortCircuitIfNoResults() {
     398           0 :       return false;
     399             :     }
     400             :   }
     401             : 
     402         150 :   private class ByFullName implements Searcher<AccountState> {
     403             :     @Override
     404             :     public boolean callerMayAssumeCandidatesAreVisible() {
     405           9 :       return true; // Rely on enforceVisibility from the index.
     406             :     }
     407             : 
     408             :     @Override
     409             :     public Optional<AccountState> tryParse(String input) {
     410          33 :       List<AccountState> results =
     411          33 :           accountQueryProvider.get().enforceVisibility(true).byFullName(input);
     412          33 :       return results.size() == 1 ? Optional.of(results.get(0)) : Optional.empty();
     413             :     }
     414             : 
     415             :     @Override
     416             :     public Stream<AccountState> search(AccountState input) {
     417           9 :       return Stream.of(input);
     418             :     }
     419             : 
     420             :     @Override
     421             :     public boolean shortCircuitIfNoResults() {
     422           1 :       return false;
     423             :     }
     424             :   }
     425             : 
     426         150 :   private class ByDefaultSearch extends StringSearcher {
     427             :     @Override
     428             :     public boolean callerMayAssumeCandidatesAreVisible() {
     429          31 :       return true; // Rely on enforceVisibility from the index.
     430             :     }
     431             : 
     432             :     @Override
     433             :     protected boolean matches(String input) {
     434          31 :       return true;
     435             :     }
     436             : 
     437             :     @Override
     438             :     public Stream<AccountState> search(String input) {
     439             :       // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
     440             :       // up with a reasonable result list.
     441             :       // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
     442             :       // more strict here.
     443          31 :       boolean canSeeSecondaryEmails = false;
     444             :       try {
     445          31 :         if (permissionBackend.user(self.get()).test(GlobalPermission.MODIFY_ACCOUNT)) {
     446          30 :           canSeeSecondaryEmails = true;
     447             :         }
     448           0 :       } catch (PermissionBackendException e) {
     449             :         // remains false
     450          31 :       }
     451          31 :       return accountQueryProvider.get().enforceVisibility(true)
     452          31 :           .byDefault(input, canSeeSecondaryEmails).stream();
     453             :     }
     454             : 
     455             :     @Override
     456             :     public boolean shortCircuitIfNoResults() {
     457             :       // In practice this doesn't matter since this is the last searcher in the list, but considered
     458             :       // on its own, it doesn't necessarily need to be terminal.
     459          19 :       return false;
     460             :     }
     461             :   }
     462             : 
     463         150 :   private final ImmutableList<Searcher<?>> nameOrEmailSearchers =
     464         150 :       ImmutableList.of(
     465             :           new ByNameAndEmail(),
     466             :           new ByEmail(),
     467             :           new FromRealm(),
     468             :           new ByFullName(),
     469             :           new ByDefaultSearch());
     470             : 
     471         150 :   private final ImmutableList<Searcher<?>> searchers =
     472         150 :       ImmutableList.<Searcher<?>>builder()
     473         150 :           .add(new BySelf())
     474         150 :           .add(new ByExactAccountId())
     475         150 :           .add(new ByParenthesizedAccountId())
     476         150 :           .add(new ByUsername())
     477         150 :           .addAll(nameOrEmailSearchers)
     478         150 :           .build();
     479             : 
     480             :   private final AccountCache accountCache;
     481             :   private final AccountControl.Factory accountControlFactory;
     482             :   private final Emails emails;
     483             :   private final IdentifiedUser.GenericFactory userFactory;
     484             :   private final Provider<CurrentUser> self;
     485             :   private final Provider<InternalAccountQuery> accountQueryProvider;
     486             :   private final Realm realm;
     487             :   private final String anonymousCowardName;
     488             :   private final PermissionBackend permissionBackend;
     489             : 
     490             :   @Inject
     491             :   AccountResolver(
     492             :       AccountCache accountCache,
     493             :       Emails emails,
     494             :       AccountControl.Factory accountControlFactory,
     495             :       IdentifiedUser.GenericFactory userFactory,
     496             :       Provider<CurrentUser> self,
     497             :       Provider<InternalAccountQuery> accountQueryProvider,
     498             :       PermissionBackend permissionBackend,
     499             :       Realm realm,
     500         150 :       @AnonymousCowardName String anonymousCowardName) {
     501         150 :     this.accountCache = accountCache;
     502         150 :     this.emails = emails;
     503         150 :     this.accountControlFactory = accountControlFactory;
     504         150 :     this.userFactory = userFactory;
     505         150 :     this.self = self;
     506         150 :     this.accountQueryProvider = accountQueryProvider;
     507         150 :     this.permissionBackend = permissionBackend;
     508         150 :     this.realm = realm;
     509         150 :     this.anonymousCowardName = anonymousCowardName;
     510         150 :   }
     511             : 
     512             :   /**
     513             :    * Resolves all accounts matching the input string, visible to the current user.
     514             :    *
     515             :    * <p>The following input formats are recognized:
     516             :    *
     517             :    * <ul>
     518             :    *   <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
     519             :    *       IdentifiedUser}. In this case, may return exactly one inactive account.
     520             :    *   <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
     521             :    *       account. This case short-circuits if the input matches.
     522             :    *   <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
     523             :    *       case short-circuits if the input matches.
     524             :    *   <li>A username ({@code "username"}).
     525             :    *   <li>A full name and email address ({@code "Full Name <email@example>"}). This case
     526             :    *       short-circuits if the input matches.
     527             :    *   <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
     528             :    *   <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
     529             :    *   <li>A full name ({@code "Full Name"}).
     530             :    *   <li>As a fallback, a {@link
     531             :    *       com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
     532             :    *       boolean, String) default search} against the account index.
     533             :    * </ul>
     534             :    *
     535             :    * @param input input string.
     536             :    * @return a result describing matching accounts. Never null even if the result set is empty.
     537             :    * @throws ConfigInvalidException if an error occurs.
     538             :    * @throws IOException if an error occurs.
     539             :    */
     540             :   public Result resolve(String input) throws ConfigInvalidException, IOException {
     541          51 :     return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::isActive);
     542             :   }
     543             : 
     544             :   public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
     545             :       throws ConfigInvalidException, IOException {
     546          10 :     return searchImpl(input, searchers, this::canSeePredicate, accountActivityPredicate);
     547             :   }
     548             : 
     549             :   /**
     550             :    * As opposed to {@link #resolve}, the returned result includes all inactive accounts for the
     551             :    * input search.
     552             :    *
     553             :    * <p>This can be used to resolve Gerrit Account from email to its {@link
     554             :    * com.google.gerrit.entities.Account.Id}, to make sure that if {@link Account} with such email
     555             :    * exists in Gerrit (even inactive), user data (email address) won't be recorded as it is, but
     556             :    * instead will be stored as a link to the corresponding Gerrit Account.
     557             :    */
     558             :   public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
     559          39 :     return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
     560             :   }
     561             : 
     562             :   public Result resolveIncludeInactiveIgnoreVisibility(String input)
     563             :       throws ConfigInvalidException, IOException {
     564           4 :     return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::allVisible);
     565             :   }
     566             : 
     567             :   public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
     568          35 :     return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
     569             :   }
     570             : 
     571             :   public Result resolveIgnoreVisibility(
     572             :       String input, Predicate<AccountState> accountActivityPredicate)
     573             :       throws ConfigInvalidException, IOException {
     574           0 :     return searchImpl(input, searchers, this::allVisiblePredicate, accountActivityPredicate);
     575             :   }
     576             : 
     577             :   /**
     578             :    * Resolves all accounts matching the input string by name or email.
     579             :    *
     580             :    * <p>The following input formats are recognized:
     581             :    *
     582             :    * <ul>
     583             :    *   <li>A full name and email address ({@code "Full Name <email@example>"}). This case
     584             :    *       short-circuits if the input matches.
     585             :    *   <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
     586             :    *   <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
     587             :    *   <li>A full name ({@code "Full Name"}).
     588             :    *   <li>As a fallback, a {@link
     589             :    *       com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
     590             :    *       boolean, String) default search} against the account index.
     591             :    * </ul>
     592             :    *
     593             :    * @param input input string.
     594             :    * @return a result describing matching accounts. Never null even if the result set is empty.
     595             :    * @throws ConfigInvalidException if an error occurs.
     596             :    * @throws IOException if an error occurs.
     597             :    * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
     598             :    *     reevaluated.
     599             :    */
     600             :   @Deprecated
     601             :   public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
     602           1 :     return searchImpl(
     603             :         input, nameOrEmailSearchers, this::canSeePredicate, AccountResolver::isActive);
     604             :   }
     605             : 
     606             :   /**
     607             :    * Same as {@link #resolveByNameOrEmail(String)}, but with exact matching for the full name, email
     608             :    * and full name.
     609             :    *
     610             :    * @param input input string.
     611             :    * @return a result describing matching accounts. Never null even if the result set is empty.
     612             :    * @throws ConfigInvalidException if an error occurs.
     613             :    * @throws IOException if an error occurs.
     614             :    * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
     615             :    *     reevaluated.
     616             :    */
     617             :   @Deprecated
     618             :   public Result resolveByExactNameOrEmail(String input) throws ConfigInvalidException, IOException {
     619           5 :     return searchImpl(
     620             :         input,
     621           5 :         ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()),
     622             :         this::canSeePredicate,
     623             :         AccountResolver::isActive);
     624             :   }
     625             : 
     626             :   private Predicate<AccountState> canSeePredicate() {
     627          61 :     return this::canSee;
     628             :   }
     629             : 
     630             :   private boolean canSee(AccountState accountState) {
     631          60 :     return accountControlFactory.get().canSee(accountState);
     632             :   }
     633             : 
     634             :   private Predicate<AccountState> allVisiblePredicate() {
     635          25 :     return AccountResolver::allVisible;
     636             :   }
     637             : 
     638             :   /** @param accountState account state for which the visibility should be checked */
     639             :   private static boolean allVisible(AccountState accountState) {
     640          39 :     return true;
     641             :   }
     642             : 
     643             :   private static boolean isActive(AccountState accountState) {
     644          51 :     return accountState.account().isActive();
     645             :   }
     646             : 
     647             :   @VisibleForTesting
     648             :   Result searchImpl(
     649             :       String input,
     650             :       ImmutableList<Searcher<?>> searchers,
     651             :       Supplier<Predicate<AccountState>> visibilitySupplier,
     652             :       Predicate<AccountState> accountActivityPredicate)
     653             :       throws ConfigInvalidException, IOException {
     654          71 :     visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
     655          71 :     List<AccountState> inactive = new ArrayList<>();
     656             : 
     657          71 :     for (Searcher<?> searcher : searchers) {
     658          71 :       Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
     659          71 :       if (!maybeResults.isPresent()) {
     660          69 :         continue;
     661             :       }
     662          71 :       Stream<AccountState> results = maybeResults.get();
     663             : 
     664          71 :       if (!searcher.callerMayAssumeCandidatesAreVisible()) {
     665          63 :         results = results.filter(visibilitySupplier.get());
     666             :       }
     667             : 
     668             :       List<AccountState> list;
     669          71 :       if (searcher.callerShouldFilterOutInactiveCandidates()) {
     670             :         // Keep track of all inactive candidates discovered by any searchers. If we end up short-
     671             :         // circuiting, the inactive list will be discarded.
     672          58 :         List<AccountState> active = new ArrayList<>();
     673          58 :         results.forEach(a -> (accountActivityPredicate.test(a) ? active : inactive).add(a));
     674          58 :         list = active;
     675          58 :       } else {
     676          47 :         list = results.collect(toImmutableList());
     677             :       }
     678             : 
     679          71 :       if (!list.isEmpty()) {
     680          70 :         return createResult(input, list);
     681             :       }
     682          46 :       if (searcher.shortCircuitIfNoResults()) {
     683             :         // For a short-circuiting searcher, return results even if empty.
     684          17 :         return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
     685             :       }
     686          46 :     }
     687          20 :     return emptyResult(input, inactive);
     688             :   }
     689             : 
     690             :   private Result createResult(String input, List<AccountState> list) {
     691          70 :     return new Result(input, list, ImmutableList.of());
     692             :   }
     693             : 
     694             :   private Result emptyResult(String input, List<AccountState> inactive) {
     695          20 :     return new Result(input, ImmutableList.of(), inactive);
     696             :   }
     697             : 
     698             :   private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
     699          43 :     return accountCache.get(ids).values().stream();
     700             :   }
     701             : }

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