Allow to delegate modify account commands to non administrators

Currently only administrators groups allow to modify accounts. This
is impractical on very big installations. Add modify account global
capability and allow administrators to optionally delegate this job.

Bug: Issue 2786
Change-Id: Icc105c39e76908a075e89ec14921972b91866dd4
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 8ff2eb6..2f92714 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1208,6 +1208,12 @@
 a replication task or a user initiated task such as an upload-pack or
 receive-pack.
 
+[[capability_modifyAccount]]
+=== Modify Account
+
+Allow to link:cmd-set-account.html[modify accounts over the ssh prompt].
+This capability allows the granted group members to modify any user account
+setting.
 
 [[capability_priority]]
 === Priority
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index 897d6889..a2944dd 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -21,7 +21,15 @@
 verification step we force within the UI.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_modifyAccount[the 'Modify Account' global capability].
+
+To set the HTTP password for the user account (option --http-password) the
+caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_generateHttpPassword[the 'Generate HTTP Password' global capability]
+in addition to 'Modify Account' global capability.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index f42811c..fccf3b3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -38,6 +38,9 @@
   /** Can create any account on the server. */
   public static final String CREATE_ACCOUNT = "createAccount";
 
+  /** Can modify any account on the server. */
+  public static final String MODIFY_ACCOUNT = "modifyAccount";
+
   /** Can create any group on the server. */
   public static final String CREATE_GROUP = "createGroup";
 
@@ -108,6 +111,7 @@
     NAMES_ALL.add(FLUSH_CACHES);
     NAMES_ALL.add(GENERATE_HTTP_PASSWORD);
     NAMES_ALL.add(KILL_TASK);
+    NAMES_ALL.add(MODIFY_ACCOUNT);
     NAMES_ALL.add(PRIORITY);
     NAMES_ALL.add(QUERY_LIMIT);
     NAMES_ALL.add(RUN_AS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 3c21d17..58c674c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -63,7 +63,7 @@
   public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
       throws AuthException, BadRequestException, OrmException, IOException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to add SSH keys");
     }
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index f0f22b5..631256a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -110,6 +110,12 @@
       || canAdministrateServer();
   }
 
+  /** @return true if the user can modify an account for another user. */
+  public boolean canModifyAccount() {
+    return canPerform(GlobalCapability.MODIFY_ACCOUNT)
+      || canAdministrateServer();
+  }
+
   /** @return true if the user can view all accounts. */
   public boolean canViewAllAccounts() {
     return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 4be8067..26beadc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -85,7 +85,7 @@
       ResourceNotFoundException, OrmException, EmailException,
       MethodNotAllowedException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to add email address");
     }
 
@@ -98,8 +98,8 @@
     }
 
     if (input.noConfirmation
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("must be administrator to use no_confirmation");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("not allowed to use no_confirmation");
     }
 
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 52ab651..abdaf23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -29,7 +29,7 @@
 
 import java.util.Collections;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class DeleteActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 6048586..f1e02bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -55,7 +55,7 @@
       throws AuthException, ResourceNotFoundException,
       ResourceConflictException, MethodNotAllowedException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to delete email address");
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 1a66277..4ac65ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -22,6 +22,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.GENERATE_HTTP_PASSWORD;
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
 import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
@@ -116,6 +117,7 @@
     have.put(FLUSH_CACHES, cc.canFlushCaches());
     have.put(GENERATE_HTTP_PASSWORD, cc.canGenerateHttpPassword());
     have.put(KILL_TASK, cc.canKillTask());
+    have.put(MODIFY_ACCOUNT, cc.canModifyAccount());
     have.put(RUN_GC, cc.canRunGC());
     have.put(STREAM_EVENTS, cc.canStreamEvents());
     have.put(VIEW_ALL_ACCOUNTS, cc.canViewAllAccounts());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
index 64991e6..bf9c9ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -40,7 +40,7 @@
   public List<EmailInfo> apply(AccountResource rsrc) throws AuthException,
       OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to list email addresses");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index 9266c3a..6846470 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -45,7 +45,7 @@
   public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
       OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to get SSH keys");
     }
     return apply(rsrc.getUser());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 69d16d8..c7a63e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -29,7 +29,7 @@
 
 import java.util.Collections;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class PutActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 554bae7..601ee76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -64,7 +64,7 @@
       throws AuthException, MethodNotAllowedException,
       ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to change name");
     }
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index 7ac987d..c49e3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -52,7 +52,7 @@
   public Response<String> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to set preferred email address");
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 9b971e4..08386b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -68,8 +68,8 @@
   public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
       throws AuthException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
     }
     if (input == null) {
       input = new Input();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index c3cc636..d9e64da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -95,8 +95,8 @@
       throws AuthException, ResourceNotFoundException, OrmException,
       IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
     }
     if (i == null) {
       i = new Input();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index b94158f..b35c03e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -55,7 +55,7 @@
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
       throws ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new ResourceNotFoundException();
     }
     return parse(rsrc.getUser(), id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
index 2c1632f..3805a0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -31,6 +31,7 @@
   public String flushCaches;
   public String generateHttpPassword;
   public String killTask;
+  public String modifyAccount;
   public String priority;
   public String queryLimit;
   public String runAs;
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index a1e0e1d..056da87 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -7,6 +7,7 @@
 flushCaches = Flush Caches
 generateHttpPassword = Generate HTTP Password
 killTask = Kill Task
+modifyAccount = Modify Account
 priority = Priority
 queryLimit = Query Limit
 runAs = Run As
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 83d75e9..6af58f3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -55,6 +57,7 @@
 
 /** Set a user's account settings. **/
 @CommandMetaData(name = "set-account", description = "Change an account's settings")
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 final class SetAccountCommand extends BaseCommand {
 
   @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
@@ -85,9 +88,6 @@
   private String httpPassword;
 
   @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
   private IdentifiedUser.GenericFactory genericUserFactory;
 
   @Inject
@@ -128,13 +128,6 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
-        if (!currentUser.getCapabilities().canAdministrateServer()) {
-          String msg =
-              String.format(
-                  "fatal: %s does not have \"Administrator\" capability.",
-                  currentUser.getUserName());
-          throw new UnloggedFailure(1, msg);
-        }
         parseCommandLine();
         validate();
         setAccount();