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:
		
				
					committed by
					
						
						David Ostrovsky
					
				
			
			
				
	
			
			
			
						parent
						
							e1587b5887
						
					
				
				
					commit
					91180b417e
				
			@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user