diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 483363b944..5e93c7e2db 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -797,6 +797,24 @@ link:#gpg-key-info[GpgKeyInfo] entities, keyed by ID. } ---- +[[delete-gpg-key]] +=== Delete GPG Key +-- +'DELETE /accounts/link:#account-id[\{account-id\}]/gpgkeys/link:#gpg-key-id[\{gpg-key-id\}]' +-- + +Deletes a GPG key of a user. + +.Request +---- + DELETE /accounts/self/gpgkeys/AFC8A49B HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + [[list-account-capabilities]] === List Account Capabilities -- diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java index 7bc02dd235..189a283b6d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java @@ -16,6 +16,7 @@ package com.google.gerrit.acceptance.api.accounts; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; import static com.google.gerrit.server.git.gpg.PublicKeyStore.fingerprintToString; import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString; import static java.nio.charset.StandardCharsets.UTF_8; @@ -96,6 +97,19 @@ public class AccountIT extends AbstractDaemonTest { return db.accountExternalIds().byAccount(account.getId()).toList(); } + @After + public void deleteGpgKeys() throws Exception { + String ref = RefNames.REFS_GPG_KEYS; + try (Repository repo = repoManager.openRepository(allUsers)) { + if (repo.getRefDatabase().exactRef(ref) != null) { + RefUpdate ru = repo.updateRef(ref); + ru.setForceUpdate(true); + assert_().withFailureMessage("Failed to delete " + ref) + .that(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); + } + } + } + @Test public void get() throws Exception { AccountInfo info = gApi @@ -262,6 +276,23 @@ public class AccountIT extends AbstractDaemonTest { } } + @Test + public void deleteGpgKey() throws Exception { + TestKey key = TestKey.key1(); + String id = keyIdToString(key.getKeyId()); + addExternalIdEmail(admin, "test1@example.com"); + gApi.accounts().self() + .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored())); + assertKeyEquals(key, gApi.accounts().self().gpgKey(id).get()); + + gApi.accounts().self().gpgKey(id).delete(); + assertThat(gApi.accounts().self().listGpgKeys()).isEmpty(); + + exception.expect(ResourceNotFoundException.class); + exception.expectMessage(id); + gApi.accounts().self().gpgKey(id).get(); + } + private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception { try (PublicKeyStore store = publicKeyStoreProvider.get()) { Iterable keys = store.get(key.getKeyId()); diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java index fe93a8eb7c..ffdcf87399 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java @@ -20,6 +20,7 @@ import com.google.gerrit.extensions.restapi.RestApiException; public interface GpgKeyApi { GpgKeyInfo get() throws RestApiException; + void delete() throws RestApiException; /** * A default implementation which allows source compatibility @@ -30,5 +31,10 @@ public interface GpgKeyApi { public GpgKeyInfo get() throws RestApiException { throw new NotImplementedException(); } + + @Override + public void delete() throws RestApiException { + throw new NotImplementedException(); + } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteGpgKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteGpgKey.java new file mode 100644 index 0000000000..b7e63cf165 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteGpgKey.java @@ -0,0 +1,91 @@ +// Copyright (C) 2015 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.account; + +import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString; + +import com.google.common.io.BaseEncoding; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.AccountResource.GpgKey; +import com.google.gerrit.server.account.DeleteGpgKey.Input; +import com.google.gerrit.server.git.gpg.PublicKeyStore; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; + +import java.io.IOException; +import java.util.Collections; + +public class DeleteGpgKey implements RestModifyView { + public static class Input { + } + + private final Provider serverIdent; + private final Provider db; + private final Provider storeProvider; + + @Inject + DeleteGpgKey(@GerritPersonIdent Provider serverIdent, + Provider db, + Provider storeProvider) { + this.serverIdent = serverIdent; + this.db = db; + this.storeProvider = storeProvider; + } + + @Override + public Response apply(GpgKey rsrc, Input input) + throws ResourceConflictException, PGPException, OrmException, + IOException { + PGPPublicKey key = rsrc.getKeyRing().getPublicKey(); + AccountExternalId.Key extIdKey = new AccountExternalId.Key( + AccountExternalId.SCHEME_GPGKEY, + BaseEncoding.base16().encode(key.getFingerprint())); + db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey)); + + try (PublicKeyStore store = storeProvider.get()) { + store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint()); + + CommitBuilder cb = new CommitBuilder(); + PersonIdent committer = serverIdent.get(); + cb.setAuthor(rsrc.getUser().newCommitterIdent( + committer.getWhen(), committer.getTimeZone())); + cb.setCommitter(committer); + cb.setMessage("Delete public key " + keyIdToString(key.getKeyID())); + + RefUpdate.Result saveResult = store.save(cb); + switch (saveResult) { + case NO_CHANGE: + case FAST_FORWARD: + break; + default: + throw new ResourceConflictException( + "Failed to delete public key: " + saveResult); + } + } + return Response.none(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java index 161af3ad7b..137a8fe58f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java @@ -66,6 +66,7 @@ public class Module extends RestApiModule { child(ACCOUNT_KIND, "gpgkeys").to(GpgKeys.class); post(ACCOUNT_KIND, "gpgkeys").to(PostGpgKeys.class); get(GPG_KEY_KIND).to(GpgKeys.Get.class); + delete(GPG_KEY_KIND).to(DeleteGpgKey.class); get(ACCOUNT_KIND, "avatar").to(GetAvatar.class); get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgKeyApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgKeyApiImpl.java index 54e9bd09e1..e42c2f6d13 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgKeyApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgKeyApiImpl.java @@ -18,10 +18,14 @@ import com.google.gerrit.extensions.api.accounts.GpgKeyApi; import com.google.gerrit.extensions.common.GpgKeyInfo; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.account.AccountResource; +import com.google.gerrit.server.account.DeleteGpgKey; import com.google.gerrit.server.account.GpgKeys; +import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import org.bouncycastle.openpgp.PGPException; + import java.io.IOException; class GpgKeyApiImpl implements GpgKeyApi { @@ -30,13 +34,16 @@ class GpgKeyApiImpl implements GpgKeyApi { } private final GpgKeys.Get get; + private final DeleteGpgKey delete; private final AccountResource.GpgKey rsrc; @AssistedInject GpgKeyApiImpl( GpgKeys.Get get, + DeleteGpgKey delete, @Assisted AccountResource.GpgKey rsrc) { this.get = get; + this.delete = delete; this.rsrc = rsrc; } @@ -48,4 +55,13 @@ class GpgKeyApiImpl implements GpgKeyApi { throw new RestApiException("Cannot get GPG key", e); } } + + @Override + public void delete() throws RestApiException { + try { + delete.apply(rsrc, new DeleteGpgKey.Input()); + } catch (PGPException | OrmException | IOException e) { + throw new RestApiException("Cannot delete GPG key", e); + } + } }