Initial version of the account removal plugin
The account plugin for Gerrit is designed to allow companies to
improve compliance with the GDPR requirements:
- Ability for an individual to display the personal information that
Gerrit holds about him
- Ability to "self-remove" the personal information from Gerrit
NOTE: This plugin itself is not giving any GDPR certification or
compliance. You would need to read carefully the EU law and
apply to your Company context and organisation.
Change-Id: I64cdaee2803b63e05285b054cecde7be6b390149
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..bcaa2f0
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,35 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+ "//tools/bzl:plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
+
+gerrit_plugin(
+ name = "account",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: account",
+ "Gerrit-Module: com.gerritforge.gerrit.plugins.account.Module",
+ "Gerrit-SshModule: com.gerritforge.gerrit.plugins.account.SshModule",
+ ],
+ resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+ name = "account_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ tags = ["account"],
+ deps = [":account__plugin_test_deps"],
+)
+
+java_library(
+ name = "account__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":account__plugin",
+ "@mockito//jar",
+ ],
+)
diff --git a/README.md b/README.md
index 95e5d56..6704c36 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,15 @@
-# gerrit-account-plugin
-Gerrit plugin to allow self-service management of accounts
+# Account management plugin for Gerrit Code Review
+
+A plugin that allows accounts to be deleted from Gerrit via an SSH command or
+REST API.
+
+## How to build
+
+To build this plugin you need to have Bazel and Gerrit source tree. See the
+[detailed instructions](/src/main/resources/Documentation/build.md) on how to build it.
+
+## Commands
+
+This plugin adds an new [SSH command](/src/main/resources/Documentation/cmd-delete.md)
+and a [REST API](/src/main/resources/Documentation/rest-api-accounts.md) for removing
+accounts from Gerrit.
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..43d72aa
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,33 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+ maven_jar(
+ name = "mockito",
+ artifact = "org.mockito:mockito-core:2.16.0",
+ sha1 = "a022ee494c753789a1e7cae75099de81d8a5cea6",
+ deps = [
+ "@byte_buddy//jar",
+ "@byte_buddy_agent//jar",
+ "@objenesis//jar",
+ ],
+ )
+
+ BYTE_BUDDY_VERSION = "1.7.9"
+
+ maven_jar(
+ name = "byte_buddy",
+ artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+ sha1 = "51218a01a882c04d0aba8c028179cce488bbcb58",
+ )
+
+ maven_jar(
+ name = "byte_buddy_agent",
+ artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+ sha1 = "a6c65f9da7f467ee1f02ff2841ffd3155aee2fc9",
+ )
+
+ maven_jar(
+ name = "objenesis",
+ artifact = "org.objenesis:objenesis:2.6",
+ sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+ )
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/AccountPersonalInformation.java b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountPersonalInformation.java
new file mode 100644
index 0000000..a81f4c6
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountPersonalInformation.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.gerrit.server.IdentifiedUser;
+import java.util.Set;
+
+public class AccountPersonalInformation {
+ public final String fullname;
+ public final String username;
+ public final Set<String> emails;
+
+ public AccountPersonalInformation(IdentifiedUser user) {
+ this.fullname = user.getName();
+ this.username = user.getUserName();
+ this.emails = user.getEmailAddresses();
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/AccountRemover.java b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountRemover.java
new file mode 100644
index 0000000..cbf35fe
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountRemover.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.inject.ImplementedBy;
+
+@ImplementedBy(GerritAccountRemover.class)
+public interface AccountRemover {
+
+ void removeAccount(int accountId) throws Exception;
+
+ boolean canDelete(int accountId);
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/AccountResourceFactory.java b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountResourceFactory.java
new file mode 100644
index 0000000..b9b84fc
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/AccountResourceFactory.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.inject.Inject;
+
+public class AccountResourceFactory {
+ private final GenericFactory userFactory;
+
+ @Inject
+ public AccountResourceFactory(GenericFactory userFactory) {
+ this.userFactory = userFactory;
+ }
+
+ public AccountResource create(int accountId) {
+ return new AccountResource(userFactory.create(new Account.Id(accountId)));
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccount.java b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccount.java
new file mode 100644
index 0000000..0f490c0
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccount.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.inject.Inject;
+
+public class DeleteAccount implements RestModifyView<AccountResource, DeleteAccount.Input> {
+ public static class Input {
+ public String accountName;
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((accountName == null) ? 0 : accountName.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ Input other = (Input) obj;
+ if (accountName == null) {
+ if (other.accountName != null) return false;
+ } else if (!accountName.equals(other.accountName)) return false;
+ return true;
+ }
+ }
+
+ private final AccountRemover remover;
+
+ @Inject
+ public DeleteAccount(AccountRemover remover) {
+ this.remover = remover;
+ }
+
+ @Override
+ public Object apply(AccountResource resource, DeleteAccount.Input input)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ boolean removed = false;
+
+ IdentifiedUser user = resource.getUser();
+ AccountPersonalInformation accountInfo = new AccountPersonalInformation(user);
+ int accountId = user.getAccountId().get();
+ assertDeletePermission(accountId);
+
+ if (input != null && input.accountName != null && user.getName().equals(input.accountName)) {
+ remover.removeAccount(accountId);
+ removed = true;
+ }
+
+ return new DeleteAccountResponse(removed, accountInfo);
+ }
+
+ private void assertDeletePermission(int accountId) throws AuthException {
+ if (!remover.canDelete(accountId)) {
+ throw new AuthException("not allowed to delete account " + accountId);
+ }
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountCommand.java b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountCommand.java
new file mode 100644
index 0000000..186586a
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountCommand.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import java.io.PrintWriter;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@CommandMetaData(name = "delete", description = "Delete a specific account")
+public class DeleteAccountCommand extends SshCommand {
+ private static final Logger log = LoggerFactory.getLogger(DeleteAccountCommand.class);
+
+ @Argument(index = 0, required = true, metaVar = "ACCOUNT-ID", usage = "id of the account")
+ private int accountId;
+
+ @Option(
+ name = "--yes-really-delete",
+ metaVar = "ACCOUNT-NAME",
+ usage = "confirmation to delete the account"
+ )
+ private String accountName;
+
+ private final AccountResourceFactory accountFactory;
+ private final DeleteAccount deleteAccount;
+
+ @Inject
+ public DeleteAccountCommand(AccountResourceFactory accountFactory, DeleteAccount deleteAccount) {
+ this.accountFactory = accountFactory;
+ this.deleteAccount = deleteAccount;
+ }
+
+ @Override
+ protected void run() throws UnloggedFailure, Failure, Exception {
+ try {
+ AccountResource account = accountFactory.create(accountId);
+
+ DeleteAccount.Input input = new DeleteAccount.Input();
+ input.accountName = accountName;
+
+ DeleteAccountResponse resp = (DeleteAccountResponse) deleteAccount.apply(account, input);
+
+ @SuppressWarnings("resource")
+ PrintWriter out = resp.deleted ? stdout : stderr;
+ out.println("Account " + (resp.deleted ? "" : "NOT") + " deleted");
+ out.println(new Gson().toJson(resp.accountInfo));
+ } catch (Exception e) {
+ stderr.printf("FAILED (%s): %s\n", e.getClass().getName(), e.getMessage());
+ stderr.flush();
+ log.error("Unable to remove account %d", accountId, e);
+ die(e);
+ }
+ }
+
+ @VisibleForTesting
+ public void testRun(int accountId, String accountName) throws Exception {
+ this.accountId = accountId;
+ this.accountName = accountName;
+ run();
+ }
+
+ @VisibleForTesting
+ public void setPrintWriters(PrintWriter out, PrintWriter err) {
+ this.stdout = out;
+ this.stderr = err;
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountResponse.java b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountResponse.java
new file mode 100644
index 0000000..97fb1dd
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/DeleteAccountResponse.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+public class DeleteAccountResponse {
+ public final boolean deleted;
+ public final AccountPersonalInformation accountInfo;
+
+ public DeleteAccountResponse(boolean deleted, AccountPersonalInformation accountInfo) {
+ this.deleted = deleted;
+ this.accountInfo = accountInfo;
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/GerritAccountRemover.java b/src/main/java/com/gerritforge/gerrit/plugins/account/GerritAccountRemover.java
new file mode 100644
index 0000000..bf1553b
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/GerritAccountRemover.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.gerritforge.gerrit.plugins.account.permissions.DeleteAccountCapability;
+import com.gerritforge.gerrit.plugins.account.permissions.DeleteOwnAccountCapability;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.extensions.common.EmailInfo;
+import com.google.gerrit.extensions.common.SshKeyInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.PutName;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GerritAccountRemover implements AccountRemover {
+ private static final Logger log = LoggerFactory.getLogger(GerritAccountRemover.class);
+ private final Accounts accounts;
+ private final PutName putName;
+ private final AccountResourceFactory accountFactory;
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> userProvider;
+ private final String pluginName;
+
+ @Inject
+ public GerritAccountRemover(
+ GerritApi api,
+ PutName putName,
+ AccountResourceFactory accountFactory,
+ PermissionBackend permissionBackend,
+ Provider<CurrentUser> userProvider,
+ @PluginName String pluginName) {
+ this.accounts = api.accounts();
+ this.putName = putName;
+ this.accountFactory = accountFactory;
+ this.permissionBackend = permissionBackend;
+ this.userProvider = userProvider;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ public void removeAccount(int accountId) throws Exception {
+ AccountApi account = isMyAccount(accountId) ? accounts.self() : accounts.id(accountId);
+ removeAccount(account, accountId);
+ }
+
+ private boolean isMyAccount(int accountId) {
+ return userProvider.get().getAccountId().get() == accountId;
+ }
+
+ private AccountResource getAccountResource(int accountId) {
+ return isMyAccount(accountId)
+ ? new AccountResource(userProvider.get().asIdentifiedUser())
+ : accountFactory.create(accountId);
+ }
+
+ private void removeAccount(AccountApi account, int accountId) throws Exception {
+ removeAccountEmails(account);
+ removeAccountSshKeys(account);
+ removeExternalIds(account);
+ removeFullName(getAccountResource(accountId));
+ if (account.getActive()) {
+ account.setActive(false);
+ }
+ }
+
+ @Override
+ public boolean canDelete(int accountId) {
+ PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider);
+ return userPermission.testOrFalse(
+ new PluginPermission(pluginName, DeleteAccountCapability.DELETE_ACCOUNT))
+ || (userPermission.testOrFalse(
+ new PluginPermission(pluginName, DeleteOwnAccountCapability.DELETE_OWN_ACCOUNT))
+ && isMyAccount(accountId));
+ }
+
+ private void removeFullName(AccountResource userRsc) throws Exception {
+ putName.apply(userRsc, new PutName.Input());
+ }
+
+ private void removeExternalIds(AccountApi account) throws RestApiException {
+ List<String> externalIds =
+ account
+ .getExternalIds()
+ .stream()
+ .map(eid -> eid.identity)
+ .filter(eid -> !eid.startsWith(ExternalId.SCHEME_USERNAME))
+ .filter(eid -> !eid.startsWith(ExternalId.SCHEME_UUID))
+ .filter(eid -> !eid.startsWith(ExternalId.SCHEME_GERRIT))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ if (externalIds.size() > 0) {
+ account.deleteExternalIds(externalIds);
+ }
+ }
+
+ private void removeAccountSshKeys(AccountApi account) throws RestApiException {
+ List<SshKeyInfo> accountKeys = account.listSshKeys();
+ for (SshKeyInfo sshKeyInfo : accountKeys) {
+ if (sshKeyInfo != null && sshKeyInfo.valid) {
+ account.deleteSshKey(sshKeyInfo.seq);
+ }
+ }
+ }
+
+ private void removeAccountEmails(AccountApi account) throws RestApiException {
+ for (EmailInfo email : account.getEmails()) {
+ account.deleteEmail(email.email);
+ }
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/Module.java b/src/main/java/com/gerritforge/gerrit/plugins/account/Module.java
new file mode 100644
index 0000000..b715154
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/Module.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
+
+import com.gerritforge.gerrit.plugins.account.permissions.DeleteAccountCapability;
+import com.gerritforge.gerrit.plugins.account.permissions.DeleteOwnAccountCapability;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.AbstractModule;
+
+class Module extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ install(
+ new RestApiModule() {
+ @Override
+ protected void configure() {
+ delete(ACCOUNT_KIND).to(DeleteAccount.class);
+ }
+ });
+
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named(DeleteAccountCapability.DELETE_ACCOUNT))
+ .to(DeleteAccountCapability.class);
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named(DeleteOwnAccountCapability.DELETE_OWN_ACCOUNT))
+ .to(DeleteOwnAccountCapability.class);
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/SshModule.java b/src/main/java/com/gerritforge/gerrit/plugins/account/SshModule.java
new file mode 100644
index 0000000..f5fdfc0
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/SshModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+public class SshModule extends PluginCommandModule {
+ @Override
+ protected void configureCommands() {
+ command(DeleteAccountCommand.class);
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteAccountCapability.java b/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteAccountCapability.java
new file mode 100644
index 0000000..5ddbdd7
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteAccountCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account.permissions;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class DeleteAccountCapability extends CapabilityDefinition {
+ public static final String DELETE_ACCOUNT = "deleteAccount";
+
+ @Override
+ public String getDescription() {
+ return "Delete any account";
+ }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteOwnAccountCapability.java b/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteOwnAccountCapability.java
new file mode 100644
index 0000000..b97aece
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/plugins/account/permissions/DeleteOwnAccountCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account.permissions;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class DeleteOwnAccountCapability extends CapabilityDefinition {
+ public static final String DELETE_OWN_ACCOUNT = "deleteOwnAccount";
+
+ @Override
+ public String getDescription() {
+ return "Delete user's own account";
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..ae2d9f0
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,47 @@
+Provides the ability to manage accounts.
+
+Gerrit accounts need at time some maintenance for troubleshooting
+purposes or for allowing people to "be forgotten" and removing
+all their associated identities and personal information.
+
+Allow admins and users to see what Gerrit holds about accounts in its
+repository and give the ability to remove any information by preserving
+the overall consistency with existing review data.
+
+Limitations
+-----------
+
+There are a few caveats:
+
+* Removal of accounts cannot be undone
+
+ This is an irreversible action, and should be taken with extreme
+ care. Backups of the accounts repository is strongly recommended.
+
+* You cannot physically delete accounts but make sure they become anonymous.
+
+ The removal of the accounts is only a logical operation. Gerrit will
+ keep the account id and some associated external ids. All the other information
+ that allows to associate a Gerrit account to a physical person will be removed.
+ The only requirement for Gerrit is to have a username with a corresponding account_id,
+ which will always be kept in the repository for consistency with existing
+ reviews.
+
+Replication of accounts deletions
+--------------------------------
+
+This plugin does not explicitly replicate any account deletions, but it triggers
+an event when an account is deleted. The [replication plugin]
+(https://gerrit-review.googlesource.com/#/admin/projects/plugins/replication)
+will do the job by triggering the replication of the associated ref updates on the
+All-Users repository.
+
+Access
+------
+
+To be allowed to delete arbitrary projects a user must be a member of a
+group that is granted the 'Delete Account' capability (provided by this
+plugin). Users can be enabled to remove their own accounts if they are member
+of a group that is granted the 'Delete Own Account' capability (provided by this
+plugin).
+
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..119c894
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,50 @@
+Build
+=====
+
+This plugin is built with Bazel in Gerrit tree mode.
+
+## Build in Gerrit tree
+
+Clone or link this plugin and the account/external_plugin_deps.bzl into the Gerrit's /plugins source
+tree.
+
+```
+ git clone https://gerrit.googlesource.com/gerrit
+ git clone https://github.com/GerritForge/account.git
+ cd gerrit/plugin
+ ln -s ../../gerrit-account-plugin account
+ rm -f external_plugin_deps.bzl
+ ln -s account/external_plugin_deps.bzl .
+```
+
+Then issue the bazel build command:
+
+```
+ bazel build plugins/account
+```
+
+The output is created in
+
+```
+ bazel-genfiles/plugins/account/account.jar
+```
+
+To execute the tests run:
+
+```
+ bazel test plugins/account:account_tests
+```
+
+or filtering using the comma separated tags:
+
+````
+ bazel test --test_tag_filters=account //...
+````
+
+This project can be imported into the Eclipse IDE.
+Add the plugin name to the `CUSTOM_PLUGINS` set in
+Gerrit core in `tools/bzl/plugins.bzl`, and execute:
+
+```
+ ./tools/eclipse/project.py
+```
diff --git a/src/main/resources/Documentation/cmd-delete.md b/src/main/resources/Documentation/cmd-delete.md
new file mode 100644
index 0000000..12b2c3c
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-delete.md
@@ -0,0 +1,57 @@
+@PLUGIN@ delete
+===============
+
+NAME
+----
+@PLUGIN@ delete - Completely delete an account and all its associated external ids
+
+SYNOPSIS
+--------
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ delete
+ [--yes-really-delete <ACCOUNT-NAME>]
+ <ACCOUNT-ID>
+```
+
+DESCRIPTION
+-----------
+Deletes an account from the Gerrit installation, removing the associated
+external ids.
+
+ACCESS
+------
+Caller must be a member of a group that is granted the 'Delete Account'
+capability (provided by this plugin), or granted the 'Delete Own Account' to
+be able to remove its own account.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+
+`--yes-really-delete`
+: Actually perform the deletion. If omitted, the command
+ will just output information about the deletion and then
+ exit.
+
+EXAMPLES
+--------
+See if you can delete an account:
+
+```
+ $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ delete 100002
+```
+
+Completely delete an account:
+
+```
+ $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ delete --yes-really-delete "'John Doe'" 100002
+```
+
+
+SEE ALSO
+--------
+
+* [Access Control](../../../Documentation/access-control.html)
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..5365edc
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,4 @@
+Configuration
+=============
+
+The @PLUGIN@ plugin does not require any additional configuration.
diff --git a/src/main/resources/Documentation/rest-api-accounts.md b/src/main/resources/Documentation/rest-api-accounts.md
new file mode 100644
index 0000000..97665fb
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-accounts.md
@@ -0,0 +1,69 @@
+@PLUGIN@ - /accounts/ REST API
+==============================
+
+This page describes the project related REST endpoints that are added
+by the @PLUGIN@.
+
+Please also take note of the general information on the
+[REST API](../../../Documentation/rest-api.html).
+
+<a id="project-endpoints"> Account Endpoints
+--------------------------------------------
+
+### <a id="delete-project"> Delete Account
+_DELETE /accounts/[\{account-id\}](../../../Documentation/rest-api-accounts.html#account-id)_
+
+OR
+
+_POST /accounts/[\{account-id\}](../../../Documentation/rest-api-accounts.html#account-id)/@PLUGIN@~delete_
+
+Deletes an account.
+
+Options for the deletion can be specified in the request body as a
+[DeleteOptionsInput](#delete-options-input) entity.
+
+Please note that some proxies prohibit request bodies for _DELETE_
+requests. In this case, if you want to specify options, use _POST_
+to delete the project.
+
+Caller must be a member of a group that is granted the 'Delete Account'
+capability (provided by this plugin), or granted the 'Delete Own Account' to
+be able to remove its own account.
+
+#### Request
+
+```
+ DELETE /accounts/1000002 HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "account_name": "John Doe"
+ }
+```
+
+#### Response
+
+```
+ HTTP/1.1 204 No Content
+```
+
+
+<a id="json-entities">JSON Entities
+-----------------------------------
+
+### <a id="delete-options-info"></a>DeleteOptionsInfo
+
+The `DeleteOptionsInfo` entity contains options for the deletion of a
+project.
+
+* _account_name_ (optional): If set to the account full name, the account is deleted. Otherwise
+ the invocation is only a dry-run that display the account details.
+
+SEE ALSO
+--------
+
+* [Accounts related REST endpoints](../../../Documentation/rest-api-accounts.html)
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountCommandTest.java b/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountCommandTest.java
new file mode 100644
index 0000000..f8fcb38
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountCommandTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account.test;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.gerritforge.gerrit.plugins.account.AccountPersonalInformation;
+import com.gerritforge.gerrit.plugins.account.AccountResourceFactory;
+import com.gerritforge.gerrit.plugins.account.DeleteAccount;
+import com.gerritforge.gerrit.plugins.account.DeleteAccountCommand;
+import com.gerritforge.gerrit.plugins.account.DeleteAccountResponse;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import org.apache.sshd.server.Environment;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeleteAccountCommandTest {
+
+ private DeleteAccountCommand deleteAccountCommand;
+
+ @Mock private AccountResourceFactory accountFactoryMock;
+ @Mock private DeleteAccount deleteAccountMock;
+ @Mock private Environment envMock;
+ @Mock private AccountResource accountResourceMock;
+ @Mock private IdentifiedUser userMock;
+
+ @Before
+ public void setup() throws Exception {
+ deleteAccountCommand = new DeleteAccountCommand(accountFactoryMock, deleteAccountMock);
+ deleteAccountCommand.setPrintWriters(
+ new PrintWriter(new ByteArrayOutputStream()), new PrintWriter(new ByteArrayOutputStream()));
+ DeleteAccountResponse resp =
+ new DeleteAccountResponse(true, new AccountPersonalInformation(userMock));
+ when(deleteAccountMock.apply(same(accountResourceMock), any(DeleteAccount.Input.class)))
+ .thenReturn(resp);
+ }
+
+ @Test
+ public void givenValidAccount_whenStart_shouldInvokeDeleteAccount() throws Exception {
+ int expectedAccountId = 1;
+ DeleteAccount.Input expectedInput = new DeleteAccount.Input();
+ expectedInput.accountName = "First Last";
+ when(accountFactoryMock.create(expectedAccountId)).thenReturn(accountResourceMock);
+
+ deleteAccountCommand.testRun(expectedAccountId, expectedInput.accountName);
+
+ verify(deleteAccountMock).apply(same(accountResourceMock), eq(expectedInput));
+ }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountTest.java b/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountTest.java
new file mode 100644
index 0000000..c5d6782
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/plugins/account/test/DeleteAccountTest.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2018 GerritForge Ltd
+//
+// 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.gerritforge.gerrit.plugins.account.test;
+
+import static org.mockito.Mockito.*;
+
+import com.gerritforge.gerrit.plugins.account.AccountRemover;
+import com.gerritforge.gerrit.plugins.account.DeleteAccount;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeleteAccountTest {
+
+ @Mock private AccountRemover accountRemoverMock;
+
+ @Mock private AccountResource accountResourceMock;
+
+ @Mock private IdentifiedUser userMock;
+
+ @Before
+ public void setup() {
+ when(accountResourceMock.getUser()).thenReturn(userMock);
+ }
+
+ @Test
+ public void givenAccountWithPermissions_whenRunningDeleteAccount_thenAccountRemoverIsInvoked()
+ throws Exception {
+ int accountId = 1;
+ DeleteAccount.Input input = new DeleteAccount.Input();
+ input.accountName = "First Last";
+ mockUserData(accountId, input.accountName);
+ when(accountRemoverMock.canDelete(accountId)).thenReturn(true);
+
+ new DeleteAccount(accountRemoverMock).apply(accountResourceMock, input);
+
+ verify(accountRemoverMock).removeAccount(accountId);
+ }
+
+ @Test(expected = AuthException.class)
+ public void givenAccountWithoutPermissions_whenRunningDeleteAccount_thenThrowAuthException()
+ throws Exception {
+ int accountId = 1;
+ DeleteAccount.Input input = new DeleteAccount.Input();
+ input.accountName = "First Last";
+ mockUserData(accountId, input.accountName);
+ when(accountRemoverMock.canDelete(accountId)).thenReturn(false);
+
+ new DeleteAccount(accountRemoverMock).apply(accountResourceMock, input);
+
+ verify(accountRemoverMock, times(0)).removeAccount(accountId);
+ }
+
+ @Test
+ public void givenAccountWithNonMatching_whenRunningDeleteAccount_thenAccountRemoverIsNotInvoked()
+ throws Exception {
+ int accountId = 1;
+ DeleteAccount.Input input = new DeleteAccount.Input();
+ input.accountName = "NonMatching Name";
+ mockUserData(accountId, "First Last");
+ when(accountRemoverMock.canDelete(accountId)).thenReturn(true);
+
+ new DeleteAccount(accountRemoverMock).apply(accountResourceMock, input);
+
+ verify(accountRemoverMock, times(0)).removeAccount(accountId);
+ }
+
+ private void mockUserData(int accountId, String accountName) {
+ when(userMock.getAccountId()).thenReturn(new Account.Id(accountId));
+ when(userMock.getName()).thenReturn(accountName);
+ }
+}
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..dfcbe9c
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,2 @@
+load("@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+ "classpath_collector")
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..a2e438f
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..0c7503d
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+ name = "main_classpath_collect",
+ testonly = 1,
+ deps = [
+ "//:delete-project__plugin_test_deps",
+ ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..55713c7
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright (C) 2017 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.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n delete-project -r .
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..3185cae
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+ cd $1; git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_DELETE-PROJECT_LABEL $(rev .)