Line data Source code
1 : // Copyright (C) 2015 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_USERNAME; 18 : 19 : import com.google.common.base.Strings; 20 : import com.google.gerrit.entities.Account; 21 : import com.google.gerrit.exceptions.DuplicateKeyException; 22 : import com.google.gerrit.extensions.api.accounts.UsernameInput; 23 : import com.google.gerrit.extensions.client.AccountFieldName; 24 : import com.google.gerrit.extensions.restapi.BadRequestException; 25 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 26 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 27 : import com.google.gerrit.extensions.restapi.Response; 28 : import com.google.gerrit.extensions.restapi.RestApiException; 29 : import com.google.gerrit.extensions.restapi.RestModifyView; 30 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException; 31 : import com.google.gerrit.server.CurrentUser; 32 : import com.google.gerrit.server.ServerInitiated; 33 : import com.google.gerrit.server.account.AccountResource; 34 : import com.google.gerrit.server.account.AccountsUpdate; 35 : import com.google.gerrit.server.account.Realm; 36 : import com.google.gerrit.server.account.externalids.ExternalId; 37 : import com.google.gerrit.server.account.externalids.ExternalIdFactory; 38 : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory; 39 : import com.google.gerrit.server.account.externalids.ExternalIds; 40 : import com.google.gerrit.server.permissions.GlobalPermission; 41 : import com.google.gerrit.server.permissions.PermissionBackend; 42 : import com.google.gerrit.server.permissions.PermissionBackendException; 43 : import com.google.gerrit.server.ssh.SshKeyCache; 44 : import com.google.inject.Inject; 45 : import com.google.inject.Provider; 46 : import com.google.inject.Singleton; 47 : import java.io.IOException; 48 : import java.util.Optional; 49 : import org.eclipse.jgit.errors.ConfigInvalidException; 50 : 51 : /** 52 : * REST endpoint to set the username of an account. 53 : * 54 : * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/username} requests. 55 : * 56 : * <p>Whether a username can be set depends on whether the used {@link Realm} supports this. 57 : * 58 : * <p>Once set a username cannot be changed or deleted. Changing usernames is disallowed because 59 : * they can be used in ref names that represent user-specific sandbox branches which can exist in 60 : * any repository and we have no way to find and rename those refs. 61 : */ 62 : @Singleton 63 : public class PutUsername implements RestModifyView<AccountResource, UsernameInput> { 64 : private final Provider<CurrentUser> self; 65 : private final PermissionBackend permissionBackend; 66 : private final ExternalIds externalIds; 67 : private final Provider<AccountsUpdate> accountsUpdateProvider; 68 : private final SshKeyCache sshKeyCache; 69 : private final Realm realm; 70 : private final ExternalIdFactory externalIdFactory; 71 : private final ExternalIdKeyFactory externalIdKeyFactory; 72 : 73 : @Inject 74 : PutUsername( 75 : Provider<CurrentUser> self, 76 : PermissionBackend permissionBackend, 77 : ExternalIds externalIds, 78 : @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider, 79 : SshKeyCache sshKeyCache, 80 : Realm realm, 81 : ExternalIdFactory externalIdFactory, 82 138 : ExternalIdKeyFactory externalIdKeyFactory) { 83 138 : this.self = self; 84 138 : this.permissionBackend = permissionBackend; 85 138 : this.externalIds = externalIds; 86 138 : this.accountsUpdateProvider = accountsUpdateProvider; 87 138 : this.sshKeyCache = sshKeyCache; 88 138 : this.realm = realm; 89 138 : this.externalIdFactory = externalIdFactory; 90 138 : this.externalIdKeyFactory = externalIdKeyFactory; 91 138 : } 92 : 93 : @Override 94 : public Response<String> apply(AccountResource rsrc, UsernameInput input) 95 : throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException { 96 2 : if (!self.get().hasSameAccountId(rsrc.getUser())) { 97 1 : permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER); 98 : } 99 : 100 2 : Account.Id accountId = rsrc.getUser().getAccountId(); 101 2 : if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) { 102 2 : throw new MethodNotAllowedException("Username cannot be changed."); 103 : } 104 : 105 1 : if (realm.accountBelongsToRealm(externalIds.byAccount(accountId)) 106 0 : && !realm.allowsEdit(AccountFieldName.USER_NAME)) { 107 0 : throw new MethodNotAllowedException("realm does not allow editing username"); 108 : } 109 : 110 1 : if (input == null || Strings.isNullOrEmpty(input.username)) { 111 0 : throw new BadRequestException("input required"); 112 : } 113 : 114 1 : if (!ExternalId.isValidUsername(input.username)) { 115 0 : throw new UnprocessableEntityException("invalid username"); 116 : } 117 : 118 1 : ExternalId.Key key = externalIdKeyFactory.create(SCHEME_USERNAME, input.username); 119 : try { 120 1 : accountsUpdateProvider 121 1 : .get() 122 1 : .update( 123 : "Set Username via API", 124 : accountId, 125 1 : u -> u.addExternalId(externalIdFactory.create(key, accountId, null, null))); 126 1 : } catch (DuplicateKeyException dupeErr) { 127 : // If we are using this identity, don't report the exception. 128 1 : Optional<ExternalId> other = externalIds.get(key); 129 1 : if (other.isPresent() && other.get().accountId().equals(accountId)) { 130 0 : return Response.ok(input.username); 131 : } 132 : 133 : // Otherwise, someone else has this identity. 134 1 : throw new ResourceConflictException("username already used", dupeErr); 135 1 : } 136 : 137 1 : sshKeyCache.evict(input.username); 138 1 : return Response.ok(input.username); 139 : } 140 : }