| // Copyright (C) 2023 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 com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Table; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.extensions.common.Input; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestModifyView; |
| import com.google.gerrit.gpg.PublicKeyStoreUtil; |
| import com.google.gerrit.index.query.QueryParseException; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.StarredChangesReader; |
| import com.google.gerrit.server.StarredChangesWriter; |
| import com.google.gerrit.server.UserInitiated; |
| import com.google.gerrit.server.account.AccountException; |
| import com.google.gerrit.server.account.AccountResource; |
| import com.google.gerrit.server.account.AccountSshKey; |
| import com.google.gerrit.server.account.AccountsUpdate; |
| import com.google.gerrit.server.account.VersionedAuthorizedKeys; |
| import com.google.gerrit.server.change.AccountPatchReviewStore; |
| import com.google.gerrit.server.edit.ChangeEdit; |
| import com.google.gerrit.server.edit.ChangeEditUtil; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.plugincontext.PluginItemContext; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| 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.List; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** |
| * REST endpoint for deleting an account. |
| * |
| * <p>This REST endpoint handles {@code DELETE /accounts/<account-identifier>} requests. Currently, |
| * only self deletions are allowed. |
| */ |
| @Singleton |
| public class DeleteAccount implements RestModifyView<AccountResource, Input> { |
| private final Provider<CurrentUser> self; |
| private final Provider<PersonIdent> serverIdent; |
| private final Provider<AccountsUpdate> accountsUpdateProvider; |
| private final VersionedAuthorizedKeys.Accessor authorizedKeys; |
| private final SshKeyCache sshKeyCache; |
| private final StarredChangesReader starredChangesReader; |
| private final StarredChangesWriter starredChangesWriter; |
| private final DeleteDraftCommentsUtil deleteDraftCommentsUtil; |
| private final GitRepositoryManager gitManager; |
| private final Provider<InternalChangeQuery> queryProvider; |
| private final ChangeEditUtil changeEditUtil; |
| private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore; |
| private final PublicKeyStoreUtil publicKeyStoreUtil; |
| |
| @Inject |
| public DeleteAccount( |
| Provider<CurrentUser> self, |
| @GerritPersonIdent Provider<PersonIdent> serverIdent, |
| @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider, |
| VersionedAuthorizedKeys.Accessor authorizedKeys, |
| SshKeyCache sshKeyCache, |
| StarredChangesReader starredChangesReader, |
| StarredChangesWriter starredChangesWriter, |
| DeleteDraftCommentsUtil deleteDraftCommentsUtil, |
| GitRepositoryManager gitManager, |
| Provider<InternalChangeQuery> queryProvider, |
| ChangeEditUtil changeEditUtil, |
| PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore, |
| PublicKeyStoreUtil publicKeyStoreUtil) { |
| this.self = self; |
| this.serverIdent = serverIdent; |
| this.accountsUpdateProvider = accountsUpdateProvider; |
| this.authorizedKeys = authorizedKeys; |
| this.sshKeyCache = sshKeyCache; |
| this.starredChangesReader = starredChangesReader; |
| this.starredChangesWriter = starredChangesWriter; |
| this.deleteDraftCommentsUtil = deleteDraftCommentsUtil; |
| this.gitManager = gitManager; |
| this.queryProvider = queryProvider; |
| this.changeEditUtil = changeEditUtil; |
| this.accountPatchReviewStore = accountPatchReviewStore; |
| this.publicKeyStoreUtil = publicKeyStoreUtil; |
| } |
| |
| @Override |
| @CanIgnoreReturnValue |
| public Response<?> apply(AccountResource rsrc, Input unusedInput) |
| throws AuthException, AccountException { |
| IdentifiedUser user = rsrc.getUser(); |
| if (!self.get().hasSameAccountId(user)) { |
| throw new AuthException("Delete account is only permitted for self"); |
| } |
| |
| Account.Id userId = user.getAccountId(); |
| try { |
| deletePgpKeys(user); |
| deleteSshKeys(user); |
| deleteStarredChanges(userId); |
| deleteChangeEdits(userId); |
| deleteDraftCommentsUtil.deleteDraftComments(user, null); |
| accountPatchReviewStore.run(a -> a.clearReviewedBy(userId)); |
| accountsUpdateProvider |
| .get() |
| .delete("Deleting user through `DELETE /accounts/{ID}`", user.getAccountId()); |
| } catch (Exception e) { |
| throw new AccountException("Could not delete account", e); |
| } |
| return Response.none(); |
| } |
| |
| private void deletePgpKeys(IdentifiedUser user) { |
| if (!publicKeyStoreUtil.hasInitializedPublicKeyStore()) { |
| return; |
| } |
| try { |
| List<RefUpdate.Result> deletedKeyResults = |
| publicKeyStoreUtil.deleteAllPgpKeysForUser( |
| user.getAccountId(), serverIdent.get(), serverIdent.get()); |
| for (RefUpdate.Result saveResult : deletedKeyResults) { |
| if (saveResult != RefUpdate.Result.NO_CHANGE |
| && saveResult != RefUpdate.Result.FAST_FORWARD) { |
| throw new StorageException(String.format("Failed to delete PGP key: %s", saveResult)); |
| } |
| } |
| } catch (Exception e) { |
| throw new StorageException("Failed to delete PGP keys.", e); |
| } |
| } |
| |
| private void deleteSshKeys(IdentifiedUser user) throws ConfigInvalidException, IOException { |
| List<AccountSshKey> keys = authorizedKeys.getKeys(user.getAccountId()); |
| for (AccountSshKey key : keys) { |
| authorizedKeys.deleteKey(user.getAccountId(), key.seq()); |
| } |
| user.getUserName().ifPresent(sshKeyCache::evict); |
| } |
| |
| private void deleteStarredChanges(Account.Id accountId) { |
| ImmutableSet<Change.Id> staredChanges = starredChangesReader.byAccountId(accountId, false); |
| for (Change.Id change : staredChanges) { |
| starredChangesWriter.unstar(self.get().getAccountId(), change); |
| } |
| } |
| |
| private void deleteChangeEdits(Account.Id accountId) throws IOException, QueryParseException { |
| // Note: in case of a stale index, the results of this query might be incomplete. |
| List<ChangeData> changesWithEdits = queryProvider.get().byOpenEditByUser(accountId); |
| |
| for (ChangeData cd : changesWithEdits) { |
| for (Table.Cell<Account.Id, PatchSet.Id, Ref> edit : cd.editRefs().cellSet()) { |
| if (!accountId.equals(edit.getRowKey())) { |
| continue; |
| } |
| try (Repository repo = gitManager.openRepository(cd.project()); |
| RevWalk rw = new RevWalk(repo)) { |
| RevCommit commit = rw.parseCommit(edit.getValue().getObjectId()); |
| changeEditUtil.delete( |
| new ChangeEdit( |
| cd.change(), |
| edit.getValue().getName(), |
| commit, |
| cd.patchSet(edit.getColumnKey()))); |
| } |
| } |
| } |
| } |
| } |