| // 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.acceptance.server.account; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.common.truth.Truth8.assertThat; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.config.GerritConfig; |
| import com.google.gerrit.acceptance.testsuite.account.AccountOperations; |
| import com.google.gerrit.acceptance.testsuite.account.TestAccount; |
| import com.google.gerrit.acceptance.testsuite.group.GroupOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.extensions.common.AccountVisibility; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.Sequences; |
| import com.google.gerrit.server.ServerInitiated; |
| import com.google.gerrit.server.account.AccountControl; |
| import com.google.gerrit.server.account.AccountResolver; |
| import com.google.gerrit.server.account.AccountResolver.Result; |
| import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.account.AccountsUpdate; |
| import com.google.gerrit.server.account.externalids.ExternalId; |
| import com.google.gerrit.testing.ConfigSuite; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import java.util.Locale; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.Config; |
| import org.junit.Test; |
| |
| public class AccountResolverIT extends AbstractDaemonTest { |
| @ConfigSuite.Default |
| public static Config defaultConfig() { |
| Config cfg = new Config(); |
| cfg.setEnum("accounts", null, "visibility", AccountVisibility.SAME_GROUP); |
| return cfg; |
| } |
| |
| @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider; |
| @Inject private AccountOperations accountOperations; |
| @Inject private AccountResolver accountResolver; |
| @Inject private AccountControl.Factory accountControlFactory; |
| @Inject private Provider<CurrentUser> self; |
| @Inject private GroupOperations groupOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private Sequences sequences; |
| |
| @Test |
| public void bySelf() throws Exception { |
| assertThat(resolve("Self")).isEmpty(); |
| accountOperations.newAccount().fullname("self").create(); |
| |
| Result result = resolveAsResult("self"); |
| assertThat(result.asIdSet()).containsExactly(admin.id()); |
| assertThat(result.isSelf()).isTrue(); |
| assertThat(result.asUniqueUser()).isSameInstanceAs(self.get()); |
| |
| result = resolveAsResult("me"); |
| assertThat(result.asIdSet()).containsExactly(admin.id()); |
| assertThat(result.isSelf()).isTrue(); |
| assertThat(result.asUniqueUser()).isSameInstanceAs(self.get()); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| checkBySelfFails(); |
| |
| requestScopeOperations.setApiUserInternal(); |
| checkBySelfFails(); |
| } |
| |
| private void checkBySelfFails() throws Exception { |
| for (String input : ImmutableList.of("self", "me")) { |
| Result result = resolveAsResult(input); |
| assertThat(result.asIdSet()).isEmpty(); |
| assertThat(result.isSelf()).isTrue(); |
| UnresolvableAccountException thrown = |
| assertThrows(UnresolvableAccountException.class, () -> result.asUnique()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo(String.format("Resolving account '%s' requires login", input)); |
| assertThat(thrown.isSelf()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void bySelfInactive() throws Exception { |
| gApi.accounts().id(user.id().get()).setActive(false); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| assertThat(gApi.accounts().id("self").getActive()).isFalse(); |
| |
| Result result = resolveAsResult("self"); |
| assertThat(result.asIdSet()).containsExactly(user.id()); |
| assertThat(result.isSelf()).isTrue(); |
| assertThat(result.asUniqueUser()).isSameInstanceAs(self.get()); |
| } |
| |
| @Test |
| public void byExactAccountId() throws Exception { |
| Account.Id existingId = accountOperations.newAccount().create(); |
| Account.Id idWithExistingIdAsFullname = |
| accountOperations.newAccount().fullname(existingId.toString()).create(); |
| |
| Account.Id nonexistentId = Account.id(sequences.nextAccountId()); |
| accountOperations.newAccount().fullname(nonexistentId.toString()).create(); |
| |
| assertThat(resolve(existingId)).containsExactly(existingId); |
| assertThat(resolve(nonexistentId)).isEmpty(); |
| |
| assertThat(resolveByNameOrEmail(existingId)).containsExactly(idWithExistingIdAsFullname); |
| } |
| |
| @Test |
| public void byParenthesizedAccountId() throws Exception { |
| Account.Id existingId = accountOperations.newAccount().fullname("Test User").create(); |
| accountOperations.newAccount().fullname(existingId.toString()).create(); |
| |
| Account.Id nonexistentId = Account.id(sequences.nextAccountId()); |
| accountOperations.newAccount().fullname("Any Name (" + nonexistentId + ")").create(); |
| accountOperations.newAccount().fullname(nonexistentId.toString()).create(); |
| |
| String existingInput = "Any Name (" + existingId + ")"; |
| assertThat(resolve(existingInput)).containsExactly(existingId); |
| assertThat(resolve("Any Name (" + nonexistentId + ")")).isEmpty(); |
| |
| assertThat(resolveByNameOrEmail(existingInput)).isEmpty(); |
| } |
| |
| @Test |
| public void byUsername() throws Exception { |
| String existingUsername = "myusername"; |
| Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create(); |
| Account.Id idWithExistingUsernameAsFullname = |
| accountOperations.newAccount().fullname(existingUsername).create(); |
| |
| String nonexistentUsername = "anotherusername"; |
| Account.Id idWithFullname = accountOperations.newAccount().fullname("anotherusername").create(); |
| |
| assertThat(resolve(existingUsername)).containsExactly(idWithUsername); |
| |
| // Doesn't short-circuit just because the input looks like a valid username. |
| assertThat(ExternalId.isValidUsername(nonexistentUsername)).isTrue(); |
| assertThat(resolve(nonexistentUsername)).containsExactly(idWithFullname); |
| |
| assertThat(resolveByNameOrEmail(existingUsername)) |
| .containsExactly(idWithExistingUsernameAsFullname); |
| } |
| |
| @Test |
| @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true") |
| public void byUsernameCaseInsensitive() throws Exception { |
| String existingUsername = "myusername"; |
| Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create(); |
| |
| String existingMixedCaseUsername = "MyMixedCaseUsername"; |
| Account.Id idWithMixedCaseUsername = |
| accountOperations.newAccount().username(existingMixedCaseUsername).create(); |
| |
| assertThat(resolve(existingUsername)).containsExactly(idWithUsername); |
| assertThat(resolve(existingMixedCaseUsername)).containsExactly(idWithMixedCaseUsername); |
| assertThat(resolve(existingMixedCaseUsername.toLowerCase(Locale.US))) |
| .containsExactly(idWithMixedCaseUsername); |
| } |
| |
| @Test |
| public void byNameAndEmail() throws Exception { |
| String email = name("user@example.com"); |
| Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create(); |
| accountOperations.newAccount().fullname(email).create(); |
| |
| String input = "First Last <" + email + ">"; |
| assertThat(resolve(input)).containsExactly(idWithEmail); |
| assertThat(resolveByNameOrEmail(input)).containsExactly(idWithEmail); |
| } |
| |
| @Test |
| public void byUserNameOrFullNameOrEmailExact() throws Exception { |
| String userName = "UserFoo"; |
| String fullName = "Name Foo"; |
| String email = "emailfoo@something.com"; |
| Account.Id id = |
| accountOperations |
| .newAccount() |
| .username(userName) |
| .fullname(fullName) |
| .preferredEmail(email) |
| .create(); |
| |
| // resolver returns results for exact matches |
| assertThat(resolveByExactNameOrEmail(userName)).containsExactly(id); |
| assertThat(resolveByExactNameOrEmail(fullName)).containsExactly(id); |
| assertThat(resolveByExactNameOrEmail(email)).containsExactly(id); |
| |
| // resolver does not match with prefixes |
| assertThat(resolveByExactNameOrEmail("UserF")).isEmpty(); |
| assertThat(resolveByExactNameOrEmail("Name F")).isEmpty(); |
| assertThat(resolveByExactNameOrEmail("emailf")).isEmpty(); |
| |
| /* The default name/email resolver accepts prefix matches */ |
| assertThat(resolveByNameOrEmail("emai")).containsExactly(id); |
| } |
| |
| @Test |
| public void byNameAndEmailPrefersAccountsWithMatchingFullName() throws Exception { |
| String email = name("user@example.com"); |
| Account.Id id1 = accountOperations.newAccount().fullname("Aaa Bbb").create(); |
| setPreferredEmailBypassingUniquenessCheck(id1, email); |
| Account.Id id2 = accountOperations.newAccount().fullname("Ccc Ddd").create(); |
| setPreferredEmailBypassingUniquenessCheck(id2, email); |
| |
| String input = "First Last <" + email + ">"; |
| assertThat(resolve(input)).containsExactly(id1, id2); |
| assertThat(resolveByNameOrEmail(input)).containsExactly(id1, id2); |
| |
| Account.Id id3 = accountOperations.newAccount().fullname("First Last").create(); |
| setPreferredEmailBypassingUniquenessCheck(id3, email); |
| assertThat(resolve(input)).containsExactly(id3); |
| assertThat(resolveByNameOrEmail(input)).containsExactly(id3); |
| |
| Account.Id id4 = accountOperations.newAccount().fullname("First Last").create(); |
| setPreferredEmailBypassingUniquenessCheck(id4, email); |
| assertThat(resolve(input)).containsExactly(id3, id4); |
| assertThat(resolveByNameOrEmail(input)).containsExactly(id3, id4); |
| } |
| |
| @Test |
| public void byEmail() throws Exception { |
| String email = name("user@example.com"); |
| Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create(); |
| accountOperations.newAccount().fullname(email).create(); |
| |
| assertThat(resolve(email)).containsExactly(idWithEmail); |
| assertThat(resolveByNameOrEmail(email)).containsExactly(idWithEmail); |
| } |
| |
| // Can't test for ByRealm because DefaultRealm with the default (OPENID) auth type doesn't support |
| // email expansion, so anything that would return a non-null value from DefaultRealm#lookup would |
| // just be an email address, handled by other tests. This could be avoided if we inject some sort |
| // of custom test realm instance, but the ugliness is not worth it for this small bit of test |
| // coverage. |
| |
| @Test |
| public void byFullName() throws Exception { |
| Account.Id id1 = accountOperations.newAccount().fullname("Somebodys Name").create(); |
| accountOperations.newAccount().fullname("A totally different name").create(); |
| String input = "Somebodys name"; |
| assertThat(resolve(input)).containsExactly(id1); |
| assertThat(resolveByNameOrEmail(input)).containsExactly(id1); |
| } |
| |
| @Test |
| public void byDefaultSearch() throws Exception { |
| Account.Id id1 = accountOperations.newAccount().fullname("John Doe").create(); |
| Account.Id id2 = accountOperations.newAccount().fullname("Jane Doe").create(); |
| assertThat(resolve("doe")).containsExactly(id1, id2); |
| assertThat(resolveByNameOrEmail("doe")).containsExactly(id1, id2); |
| } |
| |
| @Test |
| public void onlyExactIdReturnsInactiveAccounts() throws Exception { |
| TestAccount account = |
| accountOperations |
| .account( |
| accountOperations |
| .newAccount() |
| .fullname("Inactiveuser Name") |
| .preferredEmail("inactiveuser@example.com") |
| .username("inactiveusername") |
| .create()) |
| .get(); |
| Account.Id id = account.accountId(); |
| String nameEmail = account.fullname().get() + " <" + account.preferredEmail().get() + ">"; |
| ImmutableList<String> inputs = |
| ImmutableList.of( |
| account.fullname().get() + " (" + account.accountId() + ")", |
| account.fullname().get(), |
| account.preferredEmail().get(), |
| nameEmail, |
| Splitter.on(' ').splitToList(account.fullname().get()).get(0)); |
| |
| assertThat(resolve(account.accountId())).containsExactly(id); |
| for (String input : inputs) { |
| assertWithMessage("results for %s (active)", input).that(resolve(input)).containsExactly(id); |
| } |
| |
| gApi.accounts().id(id.get()).setActive(false); |
| assertThat(resolve(account.accountId())).containsExactly(id); |
| for (String input : inputs) { |
| Result result = accountResolver.resolve(input); |
| assertWithMessage("results for %s (inactive)", input).that(result.asIdSet()).isEmpty(); |
| UnresolvableAccountException thrown = |
| assertThrows(UnresolvableAccountException.class, () -> result.asUnique()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo( |
| "Account '" |
| + input |
| + "' only matches inactive accounts. To use an inactive account, retry" |
| + " with one of the following exact account IDs:\n" |
| + id |
| + ": " |
| + nameEmail); |
| assertWithMessage("results by name or email for %s (inactive)", input) |
| .that(resolveByNameOrEmail(input)) |
| .isEmpty(); |
| } |
| } |
| |
| @Test |
| public void filterVisibility() throws Exception { |
| Account.Id id1 = |
| accountOperations |
| .newAccount() |
| .fullname("John Doe") |
| .preferredEmail("johndoe@example.com") |
| .create(); |
| Account.Id id2 = |
| accountOperations |
| .newAccount() |
| .fullname("Jane Doe") |
| .preferredEmail("janedoe@example.com") |
| .create(); |
| |
| // Admin can see all accounts. Use a variety of searches, including with/without |
| // callerMayAssumeCandidatesAreVisible. |
| assertThat(resolve(id1)).containsExactly(id1); |
| assertThat(resolve("John Doe")).containsExactly(id1); |
| assertThat(resolve("johndoe@example.com")).containsExactly(id1); |
| assertThat(resolve(id2)).containsExactly(id2); |
| assertThat(resolve("Jane Doe")).containsExactly(id2); |
| assertThat(resolve("janedoe@example.com")).containsExactly(id2); |
| assertThat(resolve("doe")).containsExactly(id1, id2); |
| |
| // id2 can't see id1, and vice versa. |
| requestScopeOperations.setApiUser(id1); |
| assertThat(resolve(id1)).containsExactly(id1); |
| assertThat(resolve("John Doe")).containsExactly(id1); |
| assertThat(resolve("johndoe@example.com")).containsExactly(id1); |
| assertThat(resolve(id2)).isEmpty(); |
| assertThat(resolve("Jane Doe")).isEmpty(); |
| assertThat(resolve("janedoe@example.com")).isEmpty(); |
| assertThat(resolve("doe")).containsExactly(id1); |
| |
| requestScopeOperations.setApiUser(id2); |
| assertThat(resolve(id1)).isEmpty(); |
| assertThat(resolve("John Doe")).isEmpty(); |
| assertThat(resolve("johndoe@example.com")).isEmpty(); |
| assertThat(resolve(id2)).containsExactly(id2); |
| assertThat(resolve("Jane Doe")).containsExactly(id2); |
| assertThat(resolve("janedoe@example.com")).containsExactly(id2); |
| assertThat(resolve("doe")).containsExactly(id2); |
| } |
| |
| @Test |
| @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP") |
| public void resolveAsUser_byFullName_accountThatIsNotVisibleToCurrentUserIsFound() |
| throws Exception { |
| Account.Id currentUser = accountOperations.newAccount().create(); |
| Account.Id resolveAsUser = accountOperations.newAccount().create(); |
| Account.Id userToBeFound = accountOperations.newAccount().fullname("Somebodys Name").create(); |
| |
| // Create a group that contains resolveAsUser and userToBeFound, so that resolveAsUser can see |
| // userToBeFound. |
| groupOperations.newGroup().addMember(resolveAsUser).addMember(userToBeFound).create(); |
| |
| // Verify that resolveAsUser can see userToBeFound. |
| assertThat(canSee(resolveAsUser, userToBeFound)).isTrue(); |
| |
| // Verify that currentUser cannot see userToBeFound |
| assertThat(canSee(currentUser, userToBeFound)).isFalse(); |
| |
| // Resolving userToBeFound as resolveAsUser should work even if the currentUser cannot see |
| // userToBeFound. |
| requestScopeOperations.setApiUser(currentUser); |
| String input = accountOperations.account(userToBeFound).get().fullname().get(); |
| assertThat(resolveAsUser(resolveAsUser, input)).containsExactly(userToBeFound); |
| } |
| |
| private boolean canSee(Account.Id currentUser, Account.Id userToBeSeen) { |
| return accountControlFactory |
| .get(identifiedUserFactory.create(currentUser)) |
| .canSee(userToBeSeen); |
| } |
| |
| private ImmutableSet<Account.Id> resolve(Object input) throws Exception { |
| return resolveAsResult(input).asIdSet(); |
| } |
| |
| private Result resolveAsResult(Object input) throws Exception { |
| return accountResolver.resolve(input.toString()); |
| } |
| |
| private ImmutableSet<Account.Id> resolveAsUser(Account.Id resolveAsUser, Object input) |
| throws Exception { |
| return resolveAsUserAsResult(resolveAsUser, input).asIdSet(); |
| } |
| |
| private Result resolveAsUserAsResult(Account.Id resolveAsUser, Object input) throws Exception { |
| return accountResolver.resolveAsUser( |
| identifiedUserFactory.create(resolveAsUser), input.toString()); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private ImmutableSet<Account.Id> resolveByNameOrEmail(Object input) throws Exception { |
| return accountResolver.resolveByNameOrEmail(input.toString()).asIdSet(); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private ImmutableSet<Account.Id> resolveByExactNameOrEmail(Object input) throws Exception { |
| return accountResolver.resolveByExactNameOrEmail(input.toString()).asIdSet(); |
| } |
| |
| private void setPreferredEmailBypassingUniquenessCheck(Account.Id id, String email) |
| throws Exception { |
| Optional<AccountState> result = |
| accountsUpdateProvider |
| .get() |
| .update("Force set preferred email", id, (s, u) -> u.setPreferredEmail(email)); |
| assertThat(result.map(a -> a.account().preferredEmail())).hasValue(email); |
| } |
| } |