AccountManager: Allow to duplicate emails per account
Fix a recurring issue after the migration of external ids from ReviewDb to NoteDb failing with the error 'Email <email> is already assigned to account <>' and thus failing LDAP authentication. The LDAP authentication may or may not have generated the preferred email record in the main 'gerrit:<>' account. Consequently, users may have added their external id as additional 'mailto:<>' record. That was a normally supported use-case and, previously in v2.15, did not create any problems or conflicts. Starting with v2.16 the extra check for unique e-mails did go a bit too far and blocked the ability to have both the 'mailto:' record and having the same value also as preferred e-mail, which is incorrect because it is a perfectly valid use-case (see Change-Id: Ie4c677365cd). When linking or updating the preferred e-mail to an existing account, accept duplications only if both external ids belongs to the target account id. Because both e-mails (preferred e-mail and 'mailto:' record) belongs to the same account, they are not really duplicated but just a legacy of the way Gerrit was used to indicate an external 'mailto:' identity that has been selected as primary e-mail afterwards. Add unit-test that consistently reproduce the issue that many people have reported after having migrated the accounts and external-ids to All-Users. NOTE: Even though the issue has been reported on the LDAP authentication use-case, the problem lies in AccountManager and is more generic. Fix the consistency checker for the e-mail external ids when multiple duplicate entries are associated with the same account. Bug: Issue 11246 Bug: Issue 9001 Change-Id: I78bc82faa2761bc0e56a9fa54a94225c82317275
This commit is contained in:
parent
e1587b5887
commit
91180b417e
@ -226,7 +226,7 @@ public class AccountManager {
|
|||||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||||
ExternalId extIdWithNewEmail =
|
ExternalId extIdWithNewEmail =
|
||||||
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password());
|
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password());
|
||||||
checkEmailNotUsed(extIdWithNewEmail);
|
checkEmailNotUsed(extId.accountId(), extIdWithNewEmail);
|
||||||
accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
|
accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
|
||||||
|
|
||||||
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
|
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
|
||||||
@ -278,7 +278,7 @@ public class AccountManager {
|
|||||||
ExternalId extId =
|
ExternalId extId =
|
||||||
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
|
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
|
||||||
logger.atFine().log("Created external Id: %s", extId);
|
logger.atFine().log("Created external Id: %s", extId);
|
||||||
checkEmailNotUsed(extId);
|
checkEmailNotUsed(newId, extId);
|
||||||
ExternalId userNameExtId =
|
ExternalId userNameExtId =
|
||||||
who.getUserName().isPresent() ? createUsername(newId, who.getUserName().get()) : null;
|
who.getUserName().isPresent() ? createUsername(newId, who.getUserName().get()) : null;
|
||||||
|
|
||||||
@ -353,7 +353,8 @@ public class AccountManager {
|
|||||||
return ExternalId.create(SCHEME_USERNAME, username, accountId);
|
return ExternalId.create(SCHEME_USERNAME, username, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkEmailNotUsed(ExternalId extIdToBeCreated) throws IOException, AccountException {
|
private void checkEmailNotUsed(Account.Id accountId, ExternalId extIdToBeCreated)
|
||||||
|
throws IOException, AccountException {
|
||||||
String email = extIdToBeCreated.email();
|
String email = extIdToBeCreated.email();
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
return;
|
return;
|
||||||
@ -364,14 +365,18 @@ public class AccountManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.atWarning().log(
|
for (ExternalId externalId : existingExtIdsWithEmail) {
|
||||||
"Email %s is already assigned to account %s;"
|
if (externalId.accountId().get() != accountId.get()) {
|
||||||
+ " cannot create external ID %s with the same email for account %s.",
|
logger.atWarning().log(
|
||||||
email,
|
"Email %s is already assigned to account %s;"
|
||||||
existingExtIdsWithEmail.iterator().next().accountId().get(),
|
+ " cannot create external ID %s with the same email for account %s.",
|
||||||
extIdToBeCreated.key().get(),
|
email,
|
||||||
extIdToBeCreated.accountId().get());
|
externalId.accountId().get(),
|
||||||
throw new AccountException("Email '" + email + "' in use by another account");
|
extIdToBeCreated.key().get(),
|
||||||
|
extIdToBeCreated.accountId().get());
|
||||||
|
throw new AccountException("Email '" + email + "' in use by another account");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
|
private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
|
||||||
@ -412,7 +417,7 @@ public class AccountManager {
|
|||||||
} else {
|
} else {
|
||||||
ExternalId newExtId =
|
ExternalId newExtId =
|
||||||
ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
|
ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
|
||||||
checkEmailNotUsed(newExtId);
|
checkEmailNotUsed(to, newExtId);
|
||||||
accountsUpdateProvider
|
accountsUpdateProvider
|
||||||
.get()
|
.get()
|
||||||
.update(
|
.update(
|
||||||
|
@ -73,8 +73,7 @@ public class ExternalIdsConsistencyChecker {
|
|||||||
private List<ConsistencyProblemInfo> check(ExternalIdNotes extIdNotes) throws IOException {
|
private List<ConsistencyProblemInfo> check(ExternalIdNotes extIdNotes) throws IOException {
|
||||||
List<ConsistencyProblemInfo> problems = new ArrayList<>();
|
List<ConsistencyProblemInfo> problems = new ArrayList<>();
|
||||||
|
|
||||||
ListMultimap<String, ExternalId.Key> emails =
|
ListMultimap<String, ExternalId> emails = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||||
MultimapBuilder.hashKeys().arrayListValues().build();
|
|
||||||
|
|
||||||
try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
|
try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
|
||||||
NoteMap noteMap = extIdNotes.getNoteMap();
|
NoteMap noteMap = extIdNotes.getNoteMap();
|
||||||
@ -85,7 +84,11 @@ public class ExternalIdsConsistencyChecker {
|
|||||||
problems.addAll(validateExternalId(extId));
|
problems.addAll(validateExternalId(extId));
|
||||||
|
|
||||||
if (extId.email() != null) {
|
if (extId.email() != null) {
|
||||||
emails.put(extId.email(), extId.key());
|
String email = extId.email();
|
||||||
|
if (emails.get(email).stream()
|
||||||
|
.noneMatch(e -> e.accountId().get() == extId.accountId().get())) {
|
||||||
|
emails.put(email, extId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (ConfigInvalidException e) {
|
} catch (ConfigInvalidException e) {
|
||||||
addError(String.format(e.getMessage()), problems);
|
addError(String.format(e.getMessage()), problems);
|
||||||
@ -102,7 +105,7 @@ public class ExternalIdsConsistencyChecker {
|
|||||||
"Email '%s' is not unique, it's used by the following external IDs: %s",
|
"Email '%s' is not unique, it's used by the following external IDs: %s",
|
||||||
e.getKey(),
|
e.getKey(),
|
||||||
e.getValue().stream()
|
e.getValue().stream()
|
||||||
.map(k -> "'" + k.get() + "'")
|
.map(k -> "'" + k.key().get() + "'")
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect(joining(", "))),
|
.collect(joining(", "))),
|
||||||
problems));
|
problems));
|
||||||
|
@ -16,7 +16,9 @@ package com.google.gerrit.acceptance.api.accounts;
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth8.assertThat;
|
import static com.google.common.truth.Truth8.assertThat;
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||||
import com.google.gerrit.acceptance.GerritConfig;
|
import com.google.gerrit.acceptance.GerritConfig;
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
@ -35,6 +37,7 @@ import com.google.gerrit.server.account.externalids.ExternalIds;
|
|||||||
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@ -433,6 +436,44 @@ public class AccountManagerIT extends AbstractDaemonTest {
|
|||||||
assertThat(accountState.get().getAccount().getPreferredEmail()).isEqualTo(email);
|
assertThat(accountState.get().getAccount().getPreferredEmail()).isEqualTo(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canFlagExistingExternalIdMailAsPreferred() throws Exception {
|
||||||
|
String email = "foo@example.com";
|
||||||
|
|
||||||
|
// Create an account with a SCHEME_GERRIT external ID
|
||||||
|
String username = "foo";
|
||||||
|
ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
|
||||||
|
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||||||
|
accountsUpdate.insert(
|
||||||
|
"Create Test Account",
|
||||||
|
accountId,
|
||||||
|
u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
|
||||||
|
|
||||||
|
// Add the additional mail external ID with SCHEME_EMAIL
|
||||||
|
accountManager.link(accountId, AuthRequest.forEmail(email));
|
||||||
|
|
||||||
|
// Try to authenticate and update the email for the account.
|
||||||
|
// Expect that this to succeed because even if the email already exist
|
||||||
|
// it is associated to the same account-id and thus is not really
|
||||||
|
// a duplicate but simply a promotion of external id to preferred email.
|
||||||
|
AuthRequest who = AuthRequest.forUser(username);
|
||||||
|
who.setEmailAddress(email);
|
||||||
|
AuthResult authResult = accountManager.authenticate(who);
|
||||||
|
|
||||||
|
// Verify that no new accounts have been created
|
||||||
|
assertThat(authResult.isNew()).isFalse();
|
||||||
|
|
||||||
|
// Verify that the account external ids with scheme 'mailto:' contains the email
|
||||||
|
AccountState account = accounts.get(authResult.getAccountId()).get();
|
||||||
|
ImmutableSet<ExternalId> accountExternalIds = account.getExternalIds(ExternalId.SCHEME_MAILTO);
|
||||||
|
assertThat(accountExternalIds).isNotEmpty();
|
||||||
|
Set<String> emails = ExternalId.getEmails(accountExternalIds).collect(toSet());
|
||||||
|
assertThat(emails).contains(email);
|
||||||
|
|
||||||
|
// Verify the preferred email
|
||||||
|
assertThat(account.getAccount().getPreferredEmail()).isEqualTo(email);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void linkNewExternalId() throws Exception {
|
public void linkNewExternalId() throws Exception {
|
||||||
// Create an account with a SCHEME_GERRIT external ID and no email
|
// Create an account with a SCHEME_GERRIT external ID and no email
|
||||||
@ -535,7 +576,26 @@ public class AccountManagerIT extends AbstractDaemonTest {
|
|||||||
AuthRequest who = AuthRequest.forEmail(email);
|
AuthRequest who = AuthRequest.forEmail(email);
|
||||||
exception.expect(AccountException.class);
|
exception.expect(AccountException.class);
|
||||||
exception.expectMessage("Email 'foo@example.com' in use by another account");
|
exception.expectMessage("Email 'foo@example.com' in use by another account");
|
||||||
accountManager.link(accountId, who);
|
accountManager.link(accountId2, who);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowLinkingExistingExternalIdEmailAsPreferred() throws Exception {
|
||||||
|
String email = "foo@example.com";
|
||||||
|
|
||||||
|
// Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
|
||||||
|
String username = "foo";
|
||||||
|
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||||||
|
ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
|
||||||
|
accountsUpdate.insert(
|
||||||
|
"Create Test Account",
|
||||||
|
accountId,
|
||||||
|
u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId, email)));
|
||||||
|
|
||||||
|
AuthRequest who = AuthRequest.forEmail(email);
|
||||||
|
AuthResult result = accountManager.link(accountId, who);
|
||||||
|
assertThat(result.isNew()).isFalse();
|
||||||
|
assertThat(result.getAccountId().get()).isEqualTo(accountId.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertNoSuchExternalIds(ExternalId.Key... extIdKeys) throws Exception {
|
private void assertNoSuchExternalIds(ExternalId.Key... extIdKeys) throws Exception {
|
||||||
|
@ -464,6 +464,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
|
|||||||
admin.id,
|
admin.id,
|
||||||
"admin.other@example.com",
|
"admin.other@example.com",
|
||||||
"secret-password"));
|
"secret-password"));
|
||||||
|
insertExtId(ExternalId.createEmail(admin.id, "admin.other@example.com"));
|
||||||
insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
|
insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -690,7 +691,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
|
private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
|
||||||
return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, admin.email);
|
return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), user.id, admin.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalId createExternalIdWithBadPassword(String username) {
|
private ExternalId createExternalIdWithBadPassword(String username) {
|
||||||
|
Loading…
Reference in New Issue
Block a user