| // Copyright (C) 2019 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.account; | |
| import static com.google.common.base.Preconditions.checkArgument; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import static java.util.Comparator.comparing; | |
| import static java.util.Objects.requireNonNull; | |
| import static java.util.stream.Collectors.joining; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.base.Suppliers; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Streams; | |
| import com.google.gerrit.common.UsedAt; | |
| import com.google.gerrit.common.UsedAt.Project; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; | |
| import com.google.gerrit.index.Schema; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.account.externalids.ExternalId; | |
| import com.google.gerrit.server.config.AnonymousCowardName; | |
| import com.google.gerrit.server.permissions.GlobalPermission; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.query.account.InternalAccountQuery; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import java.util.TreeSet; | |
| import java.util.function.Predicate; | |
| import java.util.function.Supplier; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import java.util.stream.Stream; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| /** | |
| * Helper for resolving accounts given arbitrary user-provided input. | |
| * | |
| * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution. | |
| * The algorithm for resolving accounts from a list of formats is as follows: | |
| * | |
| * <ol> | |
| * <li>For each recognized format in the order listed in the method Javadoc, check whether the | |
| * input matches that format. | |
| * <li>If so, resolve accounts according to that format. | |
| * <li>Filter out invisible and inactive accounts. | |
| * <li>If the result list is non-empty, return. | |
| * <li>If the format is listed above as being short-circuiting, return. | |
| * <li>Otherwise, return to step 1 with the next format. | |
| * </ol> | |
| * | |
| * <p>The result never includes accounts that are not visible to the calling user. It also never | |
| * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc. | |
| */ | |
| @Singleton | |
| public class AccountResolver { | |
| public static class UnresolvableAccountException extends UnprocessableEntityException { | |
| private static final long serialVersionUID = 1L; | |
| private final Result result; | |
| @VisibleForTesting | |
| UnresolvableAccountException(Result result) { | |
| super(exceptionMessage(result)); | |
| this.result = result; | |
| } | |
| public boolean isSelf() { | |
| return result.isSelf(); | |
| } | |
| } | |
| public static String exceptionMessage(Result result) { | |
| checkArgument(result.asList().size() != 1); | |
| if (result.asList().isEmpty()) { | |
| if (result.isSelf()) { | |
| return "Resolving account '" + result.input() + "' requires login"; | |
| } | |
| if (result.filteredInactive().isEmpty()) { | |
| return "Account '" + result.input() + "' not found"; | |
| } | |
| return result.filteredInactive().stream() | |
| .map(a -> formatForException(result, a)) | |
| .collect( | |
| joining( | |
| "\n", | |
| "Account '" | |
| + result.input() | |
| + "' only matches inactive accounts. To use an inactive account, retry with" | |
| + " one of the following exact account IDs:\n", | |
| "")); | |
| } | |
| return result.asList().stream() | |
| .map(a -> formatForException(result, a)) | |
| .limit(3) | |
| .collect( | |
| joining( | |
| "\n", "Account '" + result.input() + "' is ambiguous (at most 3 shown):\n", "")); | |
| } | |
| private static String formatForException(Result result, AccountState state) { | |
| return state.account().id() | |
| + ": " | |
| + state.account().getNameEmail(result.accountResolver().anonymousCowardName); | |
| } | |
| public static boolean isSelf(String input) { | |
| return "self".equals(input) || "me".equals(input); | |
| } | |
| public class Result { | |
| private final String input; | |
| private final ImmutableList<AccountState> list; | |
| private final ImmutableList<AccountState> filteredInactive; | |
| private final CurrentUser searchedAsUser; | |
| @VisibleForTesting | |
| Result( | |
| String input, | |
| List<AccountState> list, | |
| List<AccountState> filteredInactive, | |
| CurrentUser searchedAsUser) { | |
| this.input = requireNonNull(input); | |
| this.list = canonicalize(list); | |
| this.filteredInactive = canonicalize(filteredInactive); | |
| this.searchedAsUser = requireNonNull(searchedAsUser); | |
| } | |
| private ImmutableList<AccountState> canonicalize(List<AccountState> list) { | |
| TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.account().id().get())); | |
| set.addAll(requireNonNull(list)); | |
| return ImmutableList.copyOf(set); | |
| } | |
| public String input() { | |
| return input; | |
| } | |
| public boolean isSelf() { | |
| return AccountResolver.isSelf(input); | |
| } | |
| public ImmutableList<AccountState> asList() { | |
| return list; | |
| } | |
| public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException { | |
| if (list.isEmpty()) { | |
| throw new UnresolvableAccountException(this); | |
| } | |
| return asIdSet(); | |
| } | |
| public ImmutableSet<Account.Id> asIdSet() { | |
| return list.stream().map(a -> a.account().id()).collect(toImmutableSet()); | |
| } | |
| public AccountState asUnique() throws UnresolvableAccountException { | |
| ensureUnique(); | |
| return list.get(0); | |
| } | |
| private void ensureUnique() throws UnresolvableAccountException { | |
| if (list.size() != 1) { | |
| throw new UnresolvableAccountException(this); | |
| } | |
| } | |
| private void ensureSelfIsUniqueIdentifiedUser() throws UnresolvableAccountException { | |
| ensureUnique(); | |
| if (!searchedAsUser.isIdentifiedUser()) { | |
| throw new UnresolvableAccountException(this); | |
| } | |
| } | |
| public IdentifiedUser asUniqueUser() throws UnresolvableAccountException { | |
| if (isSelf()) { | |
| ensureSelfIsUniqueIdentifiedUser(); | |
| // In the special case of "self", use the exact IdentifiedUser from the request context, to | |
| // preserve the peer address and any other per-request state. | |
| return searchedAsUser.asIdentifiedUser(); | |
| } | |
| ensureUnique(); | |
| return userFactory.create(asUnique()); | |
| } | |
| public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller) | |
| throws UnresolvableAccountException { | |
| ensureUnique(); | |
| if (isSelf()) { | |
| return searchedAsUser.asIdentifiedUser(); | |
| } | |
| return userFactory.runAs( | |
| /* remotePeer= */ null, list.get(0).account().id(), requireNonNull(caller).getRealUser()); | |
| } | |
| @VisibleForTesting | |
| ImmutableList<AccountState> filteredInactive() { | |
| return filteredInactive; | |
| } | |
| private AccountResolver accountResolver() { | |
| return AccountResolver.this; | |
| } | |
| } | |
| @VisibleForTesting | |
| interface Searcher<I> { | |
| default boolean callerShouldFilterOutInactiveCandidates() { | |
| return true; | |
| } | |
| /** | |
| * Searches can be done on behalf of either the current user or another provided user. The | |
| * results of some searchers, such as BySelf, are affected by the context user. | |
| */ | |
| default boolean requiresContextUser() { | |
| return false; | |
| } | |
| Optional<I> tryParse(String input) throws IOException; | |
| /** | |
| * This method should be implemented for every searcher which doesn't require a context user. | |
| * | |
| * @param input to search for | |
| * @return stream of the matching accounts | |
| * @throws IOException by some subclasses | |
| * @throws ConfigInvalidException by some subclasses | |
| */ | |
| default Stream<AccountState> search(I input) throws IOException, ConfigInvalidException { | |
| throw new IllegalStateException("search(I) default implementation should never be called."); | |
| } | |
| /** | |
| * This method should be implemented for every searcher which requires a context user. | |
| * | |
| * @param input to search for | |
| * @param asUser the context user for the search | |
| * @return stream of the matching accounts | |
| * @throws IOException by some subclasses | |
| * @throws ConfigInvalidException by some subclasses | |
| */ | |
| default Stream<AccountState> search(I input, CurrentUser asUser) | |
| throws IOException, ConfigInvalidException { | |
| if (!requiresContextUser()) { | |
| return search(input); | |
| } | |
| throw new IllegalStateException( | |
| "The searcher requires a context user, but doesn't implement search(input, asUser)."); | |
| } | |
| boolean shortCircuitIfNoResults(); | |
| default Optional<Stream<AccountState>> trySearch(String input, CurrentUser asUser) | |
| throws IOException, ConfigInvalidException { | |
| Optional<I> parsed = tryParse(input); | |
| if (parsed.isEmpty()) { | |
| return Optional.empty(); | |
| } | |
| return requiresContextUser() | |
| ? Optional.of(search(parsed.get(), asUser)) | |
| : Optional.of(search(parsed.get())); | |
| } | |
| } | |
| @VisibleForTesting | |
| abstract static class StringSearcher implements Searcher<String> { | |
| @Override | |
| public final Optional<String> tryParse(String input) { | |
| return matches(input) ? Optional.of(input) : Optional.empty(); | |
| } | |
| protected abstract boolean matches(String input); | |
| } | |
| private abstract class AccountIdSearcher implements Searcher<Account.Id> { | |
| @Override | |
| public final Stream<AccountState> search(Account.Id input) { | |
| return Streams.stream(accountCache.get(input)); | |
| } | |
| } | |
| private static class BySelf extends StringSearcher { | |
| @Override | |
| public boolean callerShouldFilterOutInactiveCandidates() { | |
| return false; | |
| } | |
| @Override | |
| public boolean requiresContextUser() { | |
| return true; | |
| } | |
| @Override | |
| protected boolean matches(String input) { | |
| return "self".equals(input) || "me".equals(input); | |
| } | |
| @Override | |
| public Stream<AccountState> search(String input, CurrentUser asUser) { | |
| if (!asUser.isIdentifiedUser()) { | |
| return Stream.empty(); | |
| } | |
| return Stream.of(asUser.asIdentifiedUser().state()); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return true; | |
| } | |
| } | |
| private class ByExactAccountId extends AccountIdSearcher { | |
| @Override | |
| public boolean callerShouldFilterOutInactiveCandidates() { | |
| return false; | |
| } | |
| @Override | |
| public Optional<Account.Id> tryParse(String input) { | |
| return Account.Id.tryParse(input); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return true; | |
| } | |
| } | |
| private class ByParenthesizedAccountId extends AccountIdSearcher { | |
| private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$"); | |
| @Override | |
| public Optional<Account.Id> tryParse(String input) { | |
| Matcher m = pattern.matcher(input); | |
| return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty(); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return true; | |
| } | |
| } | |
| private class ByUsername extends StringSearcher { | |
| @Override | |
| public boolean matches(String input) { | |
| return ExternalId.isValidUsername(input); | |
| } | |
| @Override | |
| public Stream<AccountState> search(String input) { | |
| return Streams.stream(accountCache.getByUsername(input)); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return false; | |
| } | |
| } | |
| private class ByNameAndEmail extends StringSearcher { | |
| @Override | |
| protected boolean matches(String input) { | |
| int lt = input.indexOf('<'); | |
| int gt = input.indexOf('>'); | |
| return lt >= 0 && gt > lt && input.contains("@"); | |
| } | |
| @Override | |
| public Stream<AccountState> search(String nameOrEmail) throws IOException { | |
| // TODO(dborowitz): This would probably work as a Searcher<Address> | |
| int lt = nameOrEmail.indexOf('<'); | |
| int gt = nameOrEmail.indexOf('>'); | |
| Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt)); | |
| ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList()); | |
| if (allMatches.isEmpty() || allMatches.size() == 1) { | |
| return allMatches.stream(); | |
| } | |
| // More than one match. If there are any that match the full name as well, return only that | |
| // subset. Otherwise, all are equally non-matching, so return the full set. | |
| if (lt == 0) { | |
| // No name was specified in the input string. | |
| return allMatches.stream(); | |
| } | |
| String name = nameOrEmail.substring(0, lt - 1); | |
| ImmutableList<AccountState> nameMatches = | |
| allMatches.stream() | |
| .filter(a -> name.equals(a.account().fullName())) | |
| .collect(toImmutableList()); | |
| return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream(); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return true; | |
| } | |
| } | |
| private class ByEmail extends StringSearcher { | |
| @Override | |
| public boolean requiresContextUser() { | |
| return true; | |
| } | |
| @Override | |
| protected boolean matches(String input) { | |
| return input.contains("@"); | |
| } | |
| @Override | |
| public Stream<AccountState> search(String input, CurrentUser asUser) throws IOException { | |
| boolean canViewSecondaryEmails = false; | |
| try { | |
| if (permissionBackend.user(asUser).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { | |
| canViewSecondaryEmails = true; | |
| } | |
| } catch (PermissionBackendException e) { | |
| // remains false | |
| } | |
| if (canViewSecondaryEmails) { | |
| return toAccountStates(emails.getAccountFor(input)); | |
| } | |
| // User cannot see secondary emails, hence search by preferred email only. | |
| List<AccountState> accountStates = accountQueryProvider.get().byPreferredEmail(input); | |
| if (accountStates.size() == 1) { | |
| return Stream.of(Iterables.getOnlyElement(accountStates)); | |
| } | |
| if (accountStates.size() > 1) { | |
| // An email can only belong to a single account. If multiple accounts are found it means | |
| // there is an inconsistency, i.e. some of the found accounts have a preferred email set | |
| // that they do not own via an external ID. Hence in this case we return only the one | |
| // account that actually owns the email via an external ID. | |
| for (AccountState accountState : accountStates) { | |
| if (accountState.externalIds().stream() | |
| .map(ExternalId::email) | |
| .filter(Objects::nonNull) | |
| .anyMatch(email -> email.equals(input))) { | |
| return Stream.of(accountState); | |
| } | |
| } | |
| // None of the matched accounts owns the email, return all matches to be consistent with | |
| // the behavior of Emails.getAccountFor(String) that is used above if the user can see | |
| // secondary emails. | |
| return accountStates.stream(); | |
| } | |
| // No match by preferred email. Since users can always see their own secondary emails, check | |
| // if the input matches a secondary email of the user and if yes, return the account of the | |
| // user. | |
| if (asUser.isIdentifiedUser() | |
| && asUser.asIdentifiedUser().state().externalIds().stream() | |
| .map(ExternalId::email) | |
| .filter(Objects::nonNull) | |
| .anyMatch(email -> email.equals(input))) { | |
| return Stream.of(asUser.asIdentifiedUser().state()); | |
| } | |
| // No match. | |
| return Stream.empty(); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return true; | |
| } | |
| } | |
| private class FromRealm extends AccountIdSearcher { | |
| @Override | |
| public Optional<Account.Id> tryParse(String input) throws IOException { | |
| return Optional.ofNullable(realm.lookup(input)); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return false; | |
| } | |
| } | |
| private class ByFullName extends StringSearcher { | |
| ByFullName() { | |
| super(); | |
| } | |
| @Override | |
| protected boolean matches(String input) { | |
| return true; | |
| } | |
| @Override | |
| public Stream<AccountState> search(String input) { | |
| return accountQueryProvider.get().byFullName(input).stream(); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| return false; | |
| } | |
| } | |
| private class ByDefaultSearch extends StringSearcher { | |
| ByDefaultSearch() { | |
| super(); | |
| } | |
| @Override | |
| public boolean requiresContextUser() { | |
| return true; | |
| } | |
| @Override | |
| protected boolean matches(String input) { | |
| return true; | |
| } | |
| @Override | |
| public Stream<AccountState> search(String input, CurrentUser asUser) { | |
| // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come | |
| // up with a reasonable result list. | |
| // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be | |
| // more strict here. | |
| boolean canViewSecondaryEmails = false; | |
| try { | |
| if (permissionBackend.user(asUser).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { | |
| canViewSecondaryEmails = true; | |
| } | |
| } catch (PermissionBackendException e) { | |
| // remains false | |
| } | |
| return accountQueryProvider.get().byDefault(input, canViewSecondaryEmails).stream(); | |
| } | |
| @Override | |
| public boolean shortCircuitIfNoResults() { | |
| // In practice this doesn't matter since this is the last searcher in the list, but considered | |
| // on its own, it doesn't necessarily need to be terminal. | |
| return false; | |
| } | |
| } | |
| private final ImmutableList<Searcher<?>> nameOrEmailSearchers = | |
| ImmutableList.of( | |
| new ByNameAndEmail(), | |
| new ByEmail(), | |
| new FromRealm(), | |
| new ByFullName(), | |
| new ByDefaultSearch()); | |
| private final ImmutableList<Searcher<?>> searchers = | |
| ImmutableList.<Searcher<?>>builder() | |
| .add(new BySelf()) | |
| .add(new ByExactAccountId()) | |
| .add(new ByParenthesizedAccountId()) | |
| .add(new ByUsername()) | |
| .addAll(nameOrEmailSearchers) | |
| .build(); | |
| private final ImmutableList<Searcher<?>> exactSearchers = | |
| ImmutableList.<Searcher<?>>builder() | |
| .add(new BySelf()) | |
| .add(new ByExactAccountId()) | |
| .add(new ByEmail()) | |
| .build(); | |
| private final AccountCache accountCache; | |
| private final AccountControl.Factory accountControlFactory; | |
| private final Emails emails; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final Provider<CurrentUser> self; | |
| private final Provider<InternalAccountQuery> accountQueryProvider; | |
| private final Realm realm; | |
| private final String anonymousCowardName; | |
| private final PermissionBackend permissionBackend; | |
| @Inject | |
| AccountResolver( | |
| AccountCache accountCache, | |
| Emails emails, | |
| AccountControl.Factory accountControlFactory, | |
| IdentifiedUser.GenericFactory userFactory, | |
| Provider<CurrentUser> self, | |
| Provider<InternalAccountQuery> accountQueryProvider, | |
| PermissionBackend permissionBackend, | |
| Realm realm, | |
| @AnonymousCowardName String anonymousCowardName) { | |
| this.accountCache = accountCache; | |
| this.emails = emails; | |
| this.accountControlFactory = accountControlFactory; | |
| this.userFactory = userFactory; | |
| this.self = self; | |
| this.accountQueryProvider = accountQueryProvider; | |
| this.permissionBackend = permissionBackend; | |
| this.realm = realm; | |
| this.anonymousCowardName = anonymousCowardName; | |
| } | |
| /** | |
| * Resolves all accounts matching the input string, visible to the current user. | |
| * | |
| * <p>The following input formats are recognized: | |
| * | |
| * <ul> | |
| * <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link | |
| * IdentifiedUser}. In this case, may return exactly one inactive account. | |
| * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive | |
| * account. This case short-circuits if the input matches. | |
| * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This | |
| * case short-circuits if the input matches. | |
| * <li>A username ({@code "username"}). | |
| * <li>A full name and email address ({@code "Full Name <email@example>"}). This case | |
| * short-circuits if the input matches. | |
| * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches. | |
| * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}. | |
| * <li>A full name ({@code "Full Name"}). | |
| * <li>As a fallback, a {@link | |
| * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema, | |
| * boolean, String) default search} against the account index. | |
| * </ul> | |
| * | |
| * @param input input string. | |
| * @return a result describing matching accounts. Never null even if the result set is empty. | |
| * @throws ConfigInvalidException if an error occurs. | |
| * @throws IOException if an error occurs. | |
| */ | |
| public Result resolve(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, searchers, self.get(), this::currentUserCanSeePredicate, AccountResolver::isActive); | |
| } | |
| /** Resolves accounts using exact searchers. Similar to the previous method. */ | |
| @UsedAt(Project.GOOGLE) | |
| public Result resolveExact(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, | |
| exactSearchers, | |
| self.get(), | |
| this::currentUserCanSeePredicate, | |
| AccountResolver::isActive); | |
| } | |
| public Result resolve(String input, Predicate<AccountState> accountActivityPredicate) | |
| throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, searchers, self.get(), this::currentUserCanSeePredicate, accountActivityPredicate); | |
| } | |
| /** | |
| * Resolves all accounts matching the input string, visible to the provided user. | |
| * | |
| * <p>The following input formats are recognized: | |
| * | |
| * <ul> | |
| * <li>The strings {@code "self"} and {@code "me"}, if the provided user is an {@link | |
| * IdentifiedUser}. In this case, may return exactly one inactive account. | |
| * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive | |
| * account. This case short-circuits if the input matches. | |
| * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This | |
| * case short-circuits if the input matches. | |
| * <li>A username ({@code "username"}). | |
| * <li>A full name and email address ({@code "Full Name <email@example>"}). This case | |
| * short-circuits if the input matches. | |
| * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches. | |
| * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}. | |
| * <li>A full name ({@code "Full Name"}). | |
| * <li>As a fallback, a {@link | |
| * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema, | |
| * boolean, String) default search} against the account index. | |
| * </ul> | |
| * | |
| * @param asUser user to resolve the users by. | |
| * @param input input string. | |
| * @return a result describing matching accounts. Never null even if the result set is empty. | |
| * @throws ConfigInvalidException if an error occurs. | |
| * @throws IOException if an error occurs. | |
| */ | |
| public Result resolveAsUser(CurrentUser asUser, String input) | |
| throws ConfigInvalidException, IOException { | |
| return resolveAsUser(asUser, input, AccountResolver::isActive); | |
| } | |
| public Result resolveAsUser( | |
| CurrentUser asUser, String input, Predicate<AccountState> accountActivityPredicate) | |
| throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, | |
| searchers, | |
| asUser, | |
| new ProvidedUserCanSeePredicate(asUser), | |
| accountActivityPredicate); | |
| } | |
| /** | |
| * As opposed to {@link #resolve}, the returned result includes all inactive accounts for the | |
| * input search. | |
| * | |
| * <p>This can be used to resolve Gerrit Account from email to its {@link | |
| * com.google.gerrit.entities.Account.Id}, to make sure that if {@link Account} with such email | |
| * exists in Gerrit (even inactive), user data (email address) won't be recorded as it is, but | |
| * instead will be stored as a link to the corresponding Gerrit Account. | |
| */ | |
| public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, | |
| searchers, | |
| self.get(), | |
| this::currentUserCanSeePredicate, | |
| AccountResolver::allVisible); | |
| } | |
| public Result resolveIncludeInactiveIgnoreVisibility(String input) | |
| throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::allVisible); | |
| } | |
| public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::isActive); | |
| } | |
| public Result resolveAsUserIgnoreVisibility(CurrentUser asUser, String input) | |
| throws ConfigInvalidException, IOException { | |
| return resolveAsUserIgnoreVisibility(asUser, input, AccountResolver::isActive); | |
| } | |
| public Result resolveAsUserIgnoreVisibility( | |
| CurrentUser asUser, String input, Predicate<AccountState> accountActivityPredicate) | |
| throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, searchers, asUser, this::allVisiblePredicate, accountActivityPredicate); | |
| } | |
| /** | |
| * Resolves all accounts matching the input string by name or email. | |
| * | |
| * <p>The following input formats are recognized: | |
| * | |
| * <ul> | |
| * <li>A full name and email address ({@code "Full Name <email@example>"}). This case | |
| * short-circuits if the input matches. | |
| * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches. | |
| * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}. | |
| * <li>A full name ({@code "Full Name"}). | |
| * <li>As a fallback, a {@link | |
| * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema, | |
| * boolean, String) default search} against the account index. | |
| * </ul> | |
| * | |
| * @param input input string. | |
| * @return a result describing matching accounts. Never null even if the result set is empty. | |
| * @throws ConfigInvalidException if an error occurs. | |
| * @throws IOException if an error occurs. | |
| * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be | |
| * reevaluated. | |
| */ | |
| @Deprecated | |
| public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, | |
| nameOrEmailSearchers, | |
| self.get(), | |
| this::currentUserCanSeePredicate, | |
| AccountResolver::isActive); | |
| } | |
| /** | |
| * Same as {@link #resolveByNameOrEmail(String)}, but with exact matching for the full name, email | |
| * and full name. | |
| * | |
| * @param input input string. | |
| * @return a result describing matching accounts. Never null even if the result set is empty. | |
| * @throws ConfigInvalidException if an error occurs. | |
| * @throws IOException if an error occurs. | |
| * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be | |
| * reevaluated. | |
| */ | |
| @Deprecated | |
| public Result resolveByExactNameOrEmail(String input) throws ConfigInvalidException, IOException { | |
| return searchImpl( | |
| input, | |
| ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()), | |
| self.get(), | |
| this::currentUserCanSeePredicate, | |
| AccountResolver::isActive); | |
| } | |
| private Predicate<AccountState> currentUserCanSeePredicate() { | |
| return accountControlFactory.get()::canSee; | |
| } | |
| private class ProvidedUserCanSeePredicate implements Supplier<Predicate<AccountState>> { | |
| CurrentUser asUser; | |
| ProvidedUserCanSeePredicate(CurrentUser asUser) { | |
| this.asUser = asUser; | |
| } | |
| @Override | |
| public Predicate<AccountState> get() { | |
| return accountControlFactory.get(asUser)::canSee; | |
| } | |
| } | |
| private Predicate<AccountState> allVisiblePredicate() { | |
| return AccountResolver::allVisible; | |
| } | |
| /** @param accountState account state for which the visibility should be checked */ | |
| private static boolean allVisible(AccountState accountState) { | |
| return true; | |
| } | |
| private static boolean isActive(AccountState accountState) { | |
| return accountState.account().isActive(); | |
| } | |
| @VisibleForTesting | |
| Result searchImpl( | |
| String input, | |
| ImmutableList<Searcher<?>> searchers, | |
| CurrentUser asUser, | |
| Supplier<Predicate<AccountState>> visibilitySupplier, | |
| Predicate<AccountState> accountActivityPredicate) | |
| throws ConfigInvalidException, IOException { | |
| requireNonNull(asUser); | |
| visibilitySupplier = Suppliers.memoize(visibilitySupplier::get); | |
| List<AccountState> inactive = new ArrayList<>(); | |
| for (Searcher<?> searcher : searchers) { | |
| Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input, asUser); | |
| if (!maybeResults.isPresent()) { | |
| continue; | |
| } | |
| Stream<AccountState> results = maybeResults.get(); | |
| // Filter out non-visible results, except if it's the BySelf searcher. Since users can always | |
| // see themselves checking the visibility is not needed for the BySelf searcher. | |
| results = searcher instanceof BySelf ? results : results.filter(visibilitySupplier.get()); | |
| List<AccountState> list; | |
| if (searcher.callerShouldFilterOutInactiveCandidates()) { | |
| // Keep track of all inactive candidates discovered by any searchers. If we end up short- | |
| // circuiting, the inactive list will be discarded. | |
| List<AccountState> active = new ArrayList<>(); | |
| results.forEach(a -> (accountActivityPredicate.test(a) ? active : inactive).add(a)); | |
| list = active; | |
| } else { | |
| list = results.collect(toImmutableList()); | |
| } | |
| if (!list.isEmpty()) { | |
| return createResult(input, list, asUser); | |
| } | |
| if (searcher.shortCircuitIfNoResults()) { | |
| // For a short-circuiting searcher, return results even if empty. | |
| return !inactive.isEmpty() | |
| ? emptyResult(input, inactive, asUser) | |
| : createResult(input, list, asUser); | |
| } | |
| } | |
| return emptyResult(input, inactive, asUser); | |
| } | |
| private Result createResult(String input, List<AccountState> list, CurrentUser searchedAsUser) { | |
| return new Result(input, list, ImmutableList.of(), searchedAsUser); | |
| } | |
| private Result emptyResult( | |
| String input, List<AccountState> inactive, CurrentUser searchedAsUser) { | |
| return new Result(input, ImmutableList.of(), inactive, searchedAsUser); | |
| } | |
| private Stream<AccountState> toAccountStates(Set<Account.Id> ids) { | |
| return accountCache.get(ids).values().stream(); | |
| } | |
| } |