Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.account; 16 : 17 : import static java.util.stream.Collectors.toList; 18 : import static java.util.stream.Collectors.toSet; 19 : 20 : import com.google.common.flogger.FluentLogger; 21 : import com.google.gerrit.extensions.common.Input; 22 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 23 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 24 : import com.google.gerrit.extensions.restapi.Response; 25 : import com.google.gerrit.extensions.restapi.RestApiException; 26 : import com.google.gerrit.extensions.restapi.RestModifyView; 27 : import com.google.gerrit.server.CurrentUser; 28 : import com.google.gerrit.server.IdentifiedUser; 29 : import com.google.gerrit.server.ServerInitiated; 30 : import com.google.gerrit.server.account.AccountResource; 31 : import com.google.gerrit.server.account.AccountState; 32 : import com.google.gerrit.server.account.AccountsUpdate; 33 : import com.google.gerrit.server.account.externalids.ExternalId; 34 : import com.google.gerrit.server.account.externalids.ExternalIdFactory; 35 : import com.google.gerrit.server.account.externalids.ExternalIds; 36 : import com.google.gerrit.server.permissions.GlobalPermission; 37 : import com.google.gerrit.server.permissions.PermissionBackend; 38 : import com.google.gerrit.server.permissions.PermissionBackendException; 39 : import com.google.inject.Inject; 40 : import com.google.inject.Provider; 41 : import com.google.inject.Singleton; 42 : import java.io.IOException; 43 : import java.util.Objects; 44 : import java.util.Optional; 45 : import java.util.Set; 46 : import java.util.concurrent.atomic.AtomicBoolean; 47 : import java.util.concurrent.atomic.AtomicReference; 48 : import org.eclipse.jgit.errors.ConfigInvalidException; 49 : 50 : /** 51 : * REST endpoint to set an email address as preferred email address for an account. 52 : * 53 : * <p>This REST endpoint handles {@code PUT 54 : * /accounts/<account-identifier>/emails/<email-identifier>/preferred} requests. 55 : * 56 : * <p>Users can only set an email address as preferred that is assigned to their account as external 57 : * ID. 58 : */ 59 : @Singleton 60 : public class PutPreferred implements RestModifyView<AccountResource.Email, Input> { 61 148 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 62 : 63 : private final Provider<CurrentUser> self; 64 : private final PermissionBackend permissionBackend; 65 : private final Provider<AccountsUpdate> accountsUpdateProvider; 66 : private final ExternalIds externalIds; 67 : private final ExternalIdFactory externalIdFactory; 68 : 69 : @Inject 70 : PutPreferred( 71 : Provider<CurrentUser> self, 72 : PermissionBackend permissionBackend, 73 : @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider, 74 : ExternalIds externalIds, 75 148 : ExternalIdFactory externalIdFactory) { 76 148 : this.self = self; 77 148 : this.permissionBackend = permissionBackend; 78 148 : this.accountsUpdateProvider = accountsUpdateProvider; 79 148 : this.externalIds = externalIds; 80 148 : this.externalIdFactory = externalIdFactory; 81 148 : } 82 : 83 : @Override 84 : public Response<String> apply(AccountResource.Email rsrc, Input input) 85 : throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException { 86 3 : if (!self.get().hasSameAccountId(rsrc.getUser())) { 87 0 : permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT); 88 : } 89 3 : return apply(rsrc.getUser(), rsrc.getEmail()); 90 : } 91 : 92 : public Response<String> apply(IdentifiedUser user, String preferredEmail) 93 : throws RestApiException, IOException, ConfigInvalidException { 94 3 : AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty()); 95 3 : AtomicBoolean alreadyPreferred = new AtomicBoolean(false); 96 3 : Optional<AccountState> updatedAccount = 97 : accountsUpdateProvider 98 3 : .get() 99 3 : .update( 100 : "Set Preferred Email via API", 101 3 : user.getAccountId(), 102 : (a, u) -> { 103 3 : if (preferredEmail.equals(a.account().preferredEmail())) { 104 1 : alreadyPreferred.set(true); 105 : } else { 106 : // check if the user has a matching email 107 2 : String matchingEmail = null; 108 : for (String email : 109 2 : a.externalIds().stream() 110 2 : .map(ExternalId::email) 111 2 : .filter(Objects::nonNull) 112 2 : .collect(toSet())) { 113 2 : if (email.equals(preferredEmail)) { 114 : // we have an email that matches exactly, prefer this one 115 2 : matchingEmail = email; 116 2 : break; 117 1 : } else if (matchingEmail == null && email.equalsIgnoreCase(preferredEmail)) { 118 : // we found an email that matches but has a different case 119 1 : matchingEmail = email; 120 : } 121 1 : } 122 : 123 2 : if (matchingEmail == null) { 124 : // user doesn't have an external ID for this email 125 1 : if (user.hasEmailAddress(preferredEmail)) { 126 : // but Realm says the user is allowed to use this email 127 1 : Set<ExternalId> existingExtIdsWithThisEmail = 128 1 : externalIds.byEmail(preferredEmail); 129 1 : if (!existingExtIdsWithThisEmail.isEmpty()) { 130 : // but the email is already assigned to another account 131 1 : logger.atWarning().log( 132 : "Cannot set preferred email %s for account %s because it is owned" 133 : + " by the following account(s): %s", 134 : preferredEmail, 135 1 : user.getAccountId(), 136 1 : existingExtIdsWithThisEmail.stream() 137 1 : .map(ExternalId::accountId) 138 1 : .collect(toList())); 139 1 : exception.set( 140 1 : Optional.of( 141 : new ResourceConflictException( 142 : "email in use by another account"))); 143 1 : return; 144 : } 145 : 146 : // claim the email now 147 1 : u.addExternalId( 148 1 : externalIdFactory.createEmail(a.account().id(), preferredEmail)); 149 1 : matchingEmail = preferredEmail; 150 1 : } else { 151 : // Realm says that the email doesn't belong to the user. This can only 152 : // happen as 153 : // a race condition because EmailsCollection would have thrown 154 : // ResourceNotFoundException already before invoking this REST endpoint. 155 0 : exception.set(Optional.of(new ResourceNotFoundException(preferredEmail))); 156 0 : return; 157 : } 158 : } 159 2 : u.setPreferredEmail(matchingEmail); 160 : } 161 3 : }); 162 3 : if (!updatedAccount.isPresent()) { 163 0 : throw new ResourceNotFoundException("account not found"); 164 : } 165 3 : if (exception.get().isPresent()) { 166 1 : throw exception.get().get(); 167 : } 168 3 : return alreadyPreferred.get() ? Response.ok() : Response.created(); 169 : } 170 : }