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:
Luca Milanesio
2019-09-25 23:49:40 +01:00
committed by David Ostrovsky
parent e1587b5887
commit 91180b417e
4 changed files with 87 additions and 18 deletions

View File

@@ -226,7 +226,7 @@ public class AccountManager {
if (newEmail != null && !newEmail.equals(oldEmail)) {
ExternalId extIdWithNewEmail =
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password());
checkEmailNotUsed(extIdWithNewEmail);
checkEmailNotUsed(extId.accountId(), extIdWithNewEmail);
accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
@@ -278,7 +278,7 @@ public class AccountManager {
ExternalId extId =
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
logger.atFine().log("Created external Id: %s", extId);
checkEmailNotUsed(extId);
checkEmailNotUsed(newId, extId);
ExternalId userNameExtId =
who.getUserName().isPresent() ? createUsername(newId, who.getUserName().get()) : null;
@@ -353,7 +353,8 @@ public class AccountManager {
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();
if (email == null) {
return;
@@ -364,14 +365,18 @@ public class AccountManager {
return;
}
logger.atWarning().log(
"Email %s is already assigned to account %s;"
+ " cannot create external ID %s with the same email for account %s.",
email,
existingExtIdsWithEmail.iterator().next().accountId().get(),
extIdToBeCreated.key().get(),
extIdToBeCreated.accountId().get());
throw new AccountException("Email '" + email + "' in use by another account");
for (ExternalId externalId : existingExtIdsWithEmail) {
if (externalId.accountId().get() != accountId.get()) {
logger.atWarning().log(
"Email %s is already assigned to account %s;"
+ " cannot create external ID %s with the same email for account %s.",
email,
externalId.accountId().get(),
extIdToBeCreated.key().get(),
extIdToBeCreated.accountId().get());
throw new AccountException("Email '" + email + "' in use by another account");
}
}
}
private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
@@ -412,7 +417,7 @@ public class AccountManager {
} else {
ExternalId newExtId =
ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
checkEmailNotUsed(newExtId);
checkEmailNotUsed(to, newExtId);
accountsUpdateProvider
.get()
.update(

View File

@@ -73,8 +73,7 @@ public class ExternalIdsConsistencyChecker {
private List<ConsistencyProblemInfo> check(ExternalIdNotes extIdNotes) throws IOException {
List<ConsistencyProblemInfo> problems = new ArrayList<>();
ListMultimap<String, ExternalId.Key> emails =
MultimapBuilder.hashKeys().arrayListValues().build();
ListMultimap<String, ExternalId> emails = MultimapBuilder.hashKeys().arrayListValues().build();
try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
NoteMap noteMap = extIdNotes.getNoteMap();
@@ -85,7 +84,11 @@ public class ExternalIdsConsistencyChecker {
problems.addAll(validateExternalId(extId));
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) {
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",
e.getKey(),
e.getValue().stream()
.map(k -> "'" + k.get() + "'")
.map(k -> "'" + k.key().get() + "'")
.sorted()
.collect(joining(", "))),
problems));