| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.restapi.account; |
| |
| import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO; |
| import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.GlobalCapability; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.GroupDescription; |
| import com.google.gerrit.exceptions.InvalidSshKeyException; |
| import com.google.gerrit.exceptions.NoSuchGroupException; |
| import com.google.gerrit.extensions.annotations.RequiresCapability; |
| import com.google.gerrit.extensions.api.accounts.AccountInput; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.IdString; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestCollectionCreateView; |
| import com.google.gerrit.extensions.restapi.TopLevelResource; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.server.Sequences; |
| import com.google.gerrit.server.UserInitiated; |
| import com.google.gerrit.server.account.AccountExternalIdCreator; |
| import com.google.gerrit.server.account.AccountLoader; |
| import com.google.gerrit.server.account.AccountResource; |
| import com.google.gerrit.server.account.AccountsUpdate; |
| import com.google.gerrit.server.account.VersionedAuthorizedKeys; |
| import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException; |
| import com.google.gerrit.server.account.externalids.ExternalId; |
| import com.google.gerrit.server.account.externalids.ExternalIdFactory; |
| import com.google.gerrit.server.config.AuthConfig; |
| import com.google.gerrit.server.group.GroupResolver; |
| import com.google.gerrit.server.group.db.GroupDelta; |
| import com.google.gerrit.server.group.db.GroupsUpdate; |
| import com.google.gerrit.server.mail.send.OutgoingEmailValidator; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.plugincontext.PluginSetContext; |
| import com.google.gerrit.server.ssh.SshKeyCache; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| |
| /** |
| * REST endpoint for creating a new account. |
| * |
| * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>} requests if the |
| * specified account doesn't exist yet. If it already exists, the request is handled by {@link |
| * PutAccount}. |
| */ |
| @RequiresCapability(GlobalCapability.CREATE_ACCOUNT) |
| @Singleton |
| public class CreateAccount |
| implements RestCollectionCreateView<TopLevelResource, AccountResource, AccountInput> { |
| private final Sequences seq; |
| private final GroupResolver groupResolver; |
| private final VersionedAuthorizedKeys.Accessor authorizedKeys; |
| private final SshKeyCache sshKeyCache; |
| private final Provider<AccountsUpdate> accountsUpdateProvider; |
| private final AccountLoader.Factory infoLoader; |
| private final PluginSetContext<AccountExternalIdCreator> externalIdCreators; |
| private final Provider<GroupsUpdate> groupsUpdate; |
| private final OutgoingEmailValidator validator; |
| private final AuthConfig authConfig; |
| private final ExternalIdFactory externalIdFactory; |
| |
| @Inject |
| CreateAccount( |
| Sequences seq, |
| GroupResolver groupResolver, |
| VersionedAuthorizedKeys.Accessor authorizedKeys, |
| SshKeyCache sshKeyCache, |
| @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider, |
| AccountLoader.Factory infoLoader, |
| PluginSetContext<AccountExternalIdCreator> externalIdCreators, |
| @UserInitiated Provider<GroupsUpdate> groupsUpdate, |
| OutgoingEmailValidator validator, |
| AuthConfig authConfig, |
| ExternalIdFactory externalIdFactory) { |
| this.seq = seq; |
| this.groupResolver = groupResolver; |
| this.authorizedKeys = authorizedKeys; |
| this.sshKeyCache = sshKeyCache; |
| this.accountsUpdateProvider = accountsUpdateProvider; |
| this.infoLoader = infoLoader; |
| this.externalIdCreators = externalIdCreators; |
| this.groupsUpdate = groupsUpdate; |
| this.validator = validator; |
| this.authConfig = authConfig; |
| this.externalIdFactory = externalIdFactory; |
| } |
| |
| @Override |
| public Response<AccountInfo> apply( |
| TopLevelResource rsrc, IdString id, @Nullable AccountInput input) |
| throws BadRequestException, ResourceConflictException, UnprocessableEntityException, |
| IOException, ConfigInvalidException, PermissionBackendException { |
| return apply(id, input != null ? input : new AccountInput()); |
| } |
| |
| public Response<AccountInfo> apply(IdString id, AccountInput input) |
| throws BadRequestException, ResourceConflictException, UnprocessableEntityException, |
| IOException, ConfigInvalidException, PermissionBackendException { |
| String username = applyCaseOfUsername(id.get()); |
| if (input.username != null && !username.equals(applyCaseOfUsername(input.username))) { |
| throw new BadRequestException("username must match URL"); |
| } |
| if (!ExternalId.isValidUsername(username)) { |
| throw new BadRequestException("Invalid username '" + username + "'"); |
| } |
| |
| if (input.name == null) { |
| input.name = input.username; |
| } |
| |
| Set<AccountGroup.UUID> groups = parseGroups(input.groups); |
| |
| Account.Id accountId = Account.id(seq.nextAccountId()); |
| List<ExternalId> extIds = new ArrayList<>(); |
| |
| if (input.email != null) { |
| if (!validator.isValid(input.email)) { |
| throw new BadRequestException("invalid email address"); |
| } |
| extIds.add(externalIdFactory.createEmail(accountId, input.email)); |
| } |
| |
| extIds.add(externalIdFactory.createUsername(username, accountId, input.httpPassword)); |
| externalIdCreators.runEach(c -> extIds.addAll(c.create(accountId, username, input.email))); |
| |
| try { |
| accountsUpdateProvider |
| .get() |
| .insert( |
| "Create Account via API", |
| accountId, |
| u -> u.setFullName(input.name).setPreferredEmail(input.email).addExternalIds(extIds)); |
| } catch (DuplicateExternalIdKeyException e) { |
| if (e.getDuplicateKey().isScheme(SCHEME_USERNAME)) { |
| throw new ResourceConflictException( |
| "username '" + e.getDuplicateKey().id() + "' already exists"); |
| } else if (e.getDuplicateKey().isScheme(SCHEME_MAILTO)) { |
| throw new UnprocessableEntityException( |
| "email '" + e.getDuplicateKey().id() + "' already exists"); |
| } else { |
| // AccountExternalIdCreator returned an external ID that already exists |
| throw e; |
| } |
| } |
| |
| for (AccountGroup.UUID groupUuid : groups) { |
| try { |
| addGroupMember(groupUuid, accountId); |
| } catch (NoSuchGroupException e) { |
| throw new UnprocessableEntityException(String.format("Group %s not found", groupUuid), e); |
| } |
| } |
| |
| if (input.sshKey != null) { |
| try { |
| authorizedKeys.addKey(accountId, input.sshKey); |
| sshKeyCache.evict(username); |
| } catch (InvalidSshKeyException e) { |
| throw new BadRequestException(e.getMessage()); |
| } |
| } |
| |
| AccountLoader loader = infoLoader.create(true); |
| AccountInfo info = loader.get(accountId); |
| loader.fill(); |
| return Response.created(info); |
| } |
| |
| private String applyCaseOfUsername(String username) { |
| return authConfig.isUserNameToLowerCase() ? username.toLowerCase(Locale.US) : username; |
| } |
| |
| private Set<AccountGroup.UUID> parseGroups(List<String> groups) |
| throws UnprocessableEntityException { |
| Set<AccountGroup.UUID> groupUuids = new HashSet<>(); |
| if (groups != null) { |
| for (String g : groups) { |
| GroupDescription.Internal internalGroup = groupResolver.parseInternal(g); |
| groupUuids.add(internalGroup.getGroupUUID()); |
| } |
| } |
| return groupUuids; |
| } |
| |
| private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId) |
| throws IOException, NoSuchGroupException, ConfigInvalidException { |
| GroupDelta groupDelta = |
| GroupDelta.builder() |
| .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId))) |
| .build(); |
| groupsUpdate.get().updateGroup(groupUuid, groupDelta); |
| } |
| } |