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 com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO; 18 : import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; 19 : 20 : import com.google.common.collect.ImmutableSet; 21 : import com.google.common.collect.Sets; 22 : import com.google.gerrit.common.Nullable; 23 : import com.google.gerrit.common.data.GlobalCapability; 24 : import com.google.gerrit.entities.Account; 25 : import com.google.gerrit.entities.AccountGroup; 26 : import com.google.gerrit.entities.GroupDescription; 27 : import com.google.gerrit.exceptions.InvalidSshKeyException; 28 : import com.google.gerrit.exceptions.NoSuchGroupException; 29 : import com.google.gerrit.extensions.annotations.RequiresCapability; 30 : import com.google.gerrit.extensions.api.accounts.AccountInput; 31 : import com.google.gerrit.extensions.common.AccountInfo; 32 : import com.google.gerrit.extensions.restapi.BadRequestException; 33 : import com.google.gerrit.extensions.restapi.IdString; 34 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 35 : import com.google.gerrit.extensions.restapi.Response; 36 : import com.google.gerrit.extensions.restapi.RestCollectionCreateView; 37 : import com.google.gerrit.extensions.restapi.TopLevelResource; 38 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException; 39 : import com.google.gerrit.server.UserInitiated; 40 : import com.google.gerrit.server.account.AccountExternalIdCreator; 41 : import com.google.gerrit.server.account.AccountLoader; 42 : import com.google.gerrit.server.account.AccountResource; 43 : import com.google.gerrit.server.account.AccountsUpdate; 44 : import com.google.gerrit.server.account.VersionedAuthorizedKeys; 45 : import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException; 46 : import com.google.gerrit.server.account.externalids.ExternalId; 47 : import com.google.gerrit.server.account.externalids.ExternalIdFactory; 48 : import com.google.gerrit.server.config.AuthConfig; 49 : import com.google.gerrit.server.group.GroupResolver; 50 : import com.google.gerrit.server.group.db.GroupDelta; 51 : import com.google.gerrit.server.group.db.GroupsUpdate; 52 : import com.google.gerrit.server.mail.send.OutgoingEmailValidator; 53 : import com.google.gerrit.server.notedb.Sequences; 54 : import com.google.gerrit.server.permissions.PermissionBackendException; 55 : import com.google.gerrit.server.plugincontext.PluginSetContext; 56 : import com.google.gerrit.server.ssh.SshKeyCache; 57 : import com.google.inject.Inject; 58 : import com.google.inject.Provider; 59 : import com.google.inject.Singleton; 60 : import java.io.IOException; 61 : import java.util.ArrayList; 62 : import java.util.HashSet; 63 : import java.util.List; 64 : import java.util.Locale; 65 : import java.util.Set; 66 : import org.eclipse.jgit.errors.ConfigInvalidException; 67 : 68 : /** 69 : * REST endpoint for creating a new account. 70 : * 71 : * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>} requests if the 72 : * specified account doesn't exist yet. If it already exists, the request is handled by {@link 73 : * PutAccount}. 74 : */ 75 : @RequiresCapability(GlobalCapability.CREATE_ACCOUNT) 76 : @Singleton 77 : public class CreateAccount 78 : implements RestCollectionCreateView<TopLevelResource, AccountResource, AccountInput> { 79 : private final Sequences seq; 80 : private final GroupResolver groupResolver; 81 : private final VersionedAuthorizedKeys.Accessor authorizedKeys; 82 : private final SshKeyCache sshKeyCache; 83 : private final Provider<AccountsUpdate> accountsUpdateProvider; 84 : private final AccountLoader.Factory infoLoader; 85 : private final PluginSetContext<AccountExternalIdCreator> externalIdCreators; 86 : private final Provider<GroupsUpdate> groupsUpdate; 87 : private final OutgoingEmailValidator validator; 88 : private final AuthConfig authConfig; 89 : private final ExternalIdFactory externalIdFactory; 90 : 91 : @Inject 92 : CreateAccount( 93 : Sequences seq, 94 : GroupResolver groupResolver, 95 : VersionedAuthorizedKeys.Accessor authorizedKeys, 96 : SshKeyCache sshKeyCache, 97 : @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider, 98 : AccountLoader.Factory infoLoader, 99 : PluginSetContext<AccountExternalIdCreator> externalIdCreators, 100 : @UserInitiated Provider<GroupsUpdate> groupsUpdate, 101 : OutgoingEmailValidator validator, 102 : AuthConfig authConfig, 103 149 : ExternalIdFactory externalIdFactory) { 104 149 : this.seq = seq; 105 149 : this.groupResolver = groupResolver; 106 149 : this.authorizedKeys = authorizedKeys; 107 149 : this.sshKeyCache = sshKeyCache; 108 149 : this.accountsUpdateProvider = accountsUpdateProvider; 109 149 : this.infoLoader = infoLoader; 110 149 : this.externalIdCreators = externalIdCreators; 111 149 : this.groupsUpdate = groupsUpdate; 112 149 : this.validator = validator; 113 149 : this.authConfig = authConfig; 114 149 : this.externalIdFactory = externalIdFactory; 115 149 : } 116 : 117 : @Override 118 : public Response<AccountInfo> apply( 119 : TopLevelResource rsrc, IdString id, @Nullable AccountInput input) 120 : throws BadRequestException, ResourceConflictException, UnprocessableEntityException, 121 : IOException, ConfigInvalidException, PermissionBackendException { 122 6 : return apply(id, input != null ? input : new AccountInput()); 123 : } 124 : 125 : public Response<AccountInfo> apply(IdString id, AccountInput input) 126 : throws BadRequestException, ResourceConflictException, UnprocessableEntityException, 127 : IOException, ConfigInvalidException, PermissionBackendException { 128 6 : String username = applyCaseOfUsername(id.get()); 129 6 : if (input.username != null && !username.equals(applyCaseOfUsername(input.username))) { 130 0 : throw new BadRequestException("username must match URL"); 131 : } 132 6 : if (!ExternalId.isValidUsername(username)) { 133 1 : throw new BadRequestException("Invalid username '" + username + "'"); 134 : } 135 : 136 6 : if (input.name == null) { 137 4 : input.name = input.username; 138 : } 139 : 140 6 : Set<AccountGroup.UUID> groups = parseGroups(input.groups); 141 : 142 6 : Account.Id accountId = Account.id(seq.nextAccountId()); 143 6 : List<ExternalId> extIds = new ArrayList<>(); 144 : 145 6 : if (input.email != null) { 146 3 : if (!validator.isValid(input.email)) { 147 1 : throw new BadRequestException("invalid email address"); 148 : } 149 3 : extIds.add(externalIdFactory.createEmail(accountId, input.email)); 150 : } 151 : 152 6 : extIds.add(externalIdFactory.createUsername(username, accountId, input.httpPassword)); 153 6 : externalIdCreators.runEach(c -> extIds.addAll(c.create(accountId, username, input.email))); 154 : 155 : try { 156 6 : accountsUpdateProvider 157 6 : .get() 158 6 : .insert( 159 : "Create Account via API", 160 : accountId, 161 6 : u -> u.setFullName(input.name).setPreferredEmail(input.email).addExternalIds(extIds)); 162 1 : } catch (DuplicateExternalIdKeyException e) { 163 1 : if (e.getDuplicateKey().isScheme(SCHEME_USERNAME)) { 164 1 : throw new ResourceConflictException( 165 1 : "username '" + e.getDuplicateKey().id() + "' already exists"); 166 1 : } else if (e.getDuplicateKey().isScheme(SCHEME_MAILTO)) { 167 1 : throw new UnprocessableEntityException( 168 1 : "email '" + e.getDuplicateKey().id() + "' already exists"); 169 : } else { 170 : // AccountExternalIdCreator returned an external ID that already exists 171 0 : throw e; 172 : } 173 6 : } 174 : 175 6 : for (AccountGroup.UUID groupUuid : groups) { 176 : try { 177 0 : addGroupMember(groupUuid, accountId); 178 0 : } catch (NoSuchGroupException e) { 179 0 : throw new UnprocessableEntityException(String.format("Group %s not found", groupUuid), e); 180 0 : } 181 0 : } 182 : 183 6 : if (input.sshKey != null) { 184 : try { 185 0 : authorizedKeys.addKey(accountId, input.sshKey); 186 0 : sshKeyCache.evict(username); 187 0 : } catch (InvalidSshKeyException e) { 188 0 : throw new BadRequestException(e.getMessage()); 189 0 : } 190 : } 191 : 192 6 : AccountLoader loader = infoLoader.create(true); 193 6 : AccountInfo info = loader.get(accountId); 194 6 : loader.fill(); 195 6 : return Response.created(info); 196 : } 197 : 198 : private String applyCaseOfUsername(String username) { 199 6 : return authConfig.isUserNameToLowerCase() ? username.toLowerCase(Locale.US) : username; 200 : } 201 : 202 : private Set<AccountGroup.UUID> parseGroups(List<String> groups) 203 : throws UnprocessableEntityException { 204 6 : Set<AccountGroup.UUID> groupUuids = new HashSet<>(); 205 6 : if (groups != null) { 206 0 : for (String g : groups) { 207 0 : GroupDescription.Internal internalGroup = groupResolver.parseInternal(g); 208 0 : groupUuids.add(internalGroup.getGroupUUID()); 209 0 : } 210 : } 211 6 : return groupUuids; 212 : } 213 : 214 : private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId) 215 : throws IOException, NoSuchGroupException, ConfigInvalidException { 216 : GroupDelta groupDelta = 217 0 : GroupDelta.builder() 218 0 : .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId))) 219 0 : .build(); 220 0 : groupsUpdate.get().updateGroup(groupUuid, groupDelta); 221 0 : } 222 : }