LCOV - code coverage report
Current view: top level - server/restapi/account - CreateAccount.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 51 72 70.8 %
Date: 2022-11-19 15:00:39 Functions: 6 9 66.7 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750