blob: b150491541dbf7e701fd2a6328daeb4d09b0e515 [file] [log] [blame]
// 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);
}
}