Allow to get/set service user groups from REST and WebUI

Change-Id: Ifb7aa6ccc71cec19fa519232b1fe1ba574b1198d
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
index 8ec538b..1331c12 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
@@ -15,37 +15,68 @@
 package com.googlesource.gerrit.plugins.serviceuser;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.group.GroupJson;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 
 public class GetConfig implements RestReadView<ConfigResource> {
+  private static final Logger log = LoggerFactory.getLogger(GetConfig.class);
 
   private final PluginConfig cfg;
+  private final GroupCache groupCache;
+  private final GroupJson groupJson;
 
   @Inject
   public GetConfig(PluginConfigFactory cfgFactory,
-      @PluginName String pluginName) {
+      @PluginName String pluginName, GroupCache groupCache, GroupJson groupJson) {
     this.cfg = cfgFactory.getFromGerritConfig(pluginName);
+    this.groupCache = groupCache;
+    this.groupJson = groupJson;
   }
 
   @Override
-  public ConfigInfo apply(ConfigResource rsrc) {
+  public ConfigInfo apply(ConfigResource rsrc) throws OrmException {
     ConfigInfo info = new ConfigInfo();
     info.info = Strings.emptyToNull(cfg.getString("infoMessage"));
     info.onSuccess = Strings.emptyToNull(cfg.getString("onSuccessMessage"));
     info.allowEmail = toBoolean(cfg.getBoolean("allowEmail", false));
     info.createNotes = toBoolean(cfg.getBoolean("createNotes", true));
     info.createNotesAsync = toBoolean(cfg.getBoolean("createNotesAsync", false));
+
     String[] blocked = cfg.getStringList("block");
     Arrays.sort(blocked);
     info.blockedNames = Arrays.asList(blocked);
+
+    String[] groups = cfg.getStringList("group");
+    info.groups = new TreeMap<>();
+    for (String g : groups) {
+      AccountGroup group = groupCache.get(new AccountGroup.NameKey(g));
+      if (group != null) {
+        GroupInfo groupInfo = groupJson.format(GroupDescriptions.forAccountGroup(group));
+        groupInfo.name = null;
+        info.groups.put(g, groupInfo);
+      } else {
+        log.warn(String.format("Service user group %s does not exist.", g));
+      }
+    }
+
     return info;
   }
 
@@ -60,5 +91,6 @@
     public Boolean createNotes;
     public Boolean createNotesAsync;
     public List<String> blockedNames;
+    public Map<String, GroupInfo> groups;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
index 92694c5..f7648f4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
@@ -20,6 +20,9 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.SitePaths;
@@ -44,23 +47,27 @@
     public Boolean createNotes;
     public Boolean createNotesAsync;
     public List<String> blockedNames;
+    public List<String> groups;
   }
 
   private final PluginConfigFactory cfgFactory;
   private final SitePaths sitePaths;
   private final String pluginName;
+  private final GroupCache groupCache;
 
   @Inject
   PutConfig(PluginConfigFactory cfgFactory, SitePaths sitePaths,
-      @PluginName String pluginName) throws IOException, ConfigInvalidException {
+      @PluginName String pluginName, GroupCache groupCache) throws IOException,
+      ConfigInvalidException {
     this.cfgFactory = cfgFactory;
     this.sitePaths = sitePaths;
     this.pluginName = pluginName;
+    this.groupCache = groupCache;
   }
 
   @Override
   public Response<String> apply(ConfigResource rsrc, Input input)
-      throws IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException, UnprocessableEntityException {
     FileBasedConfig cfg =
         new FileBasedConfig(sitePaths.gerrit_config, FS.DETECTED);
     cfg.load();
@@ -84,6 +91,15 @@
     if (input.blockedNames != null) {
       cfg.setStringList("plugin", pluginName, "block", input.blockedNames);
     }
+    if (input.groups != null) {
+      for (String g : input.groups) {
+        if (groupCache.get(new AccountGroup.NameKey(g)) == null) {
+          throw new UnprocessableEntityException(
+              String.format("Group %s does not exist.", g));
+        }
+      }
+      cfg.setStringList("plugin", pluginName, "group", input.groups);
+    }
     cfg.save();
     cfgFactory.getFromGerritConfig(pluginName, true);
     return Response.<String> ok("OK");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java
index 197cdae..8a01c1d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.serviceuser.client;
 
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
 
@@ -26,6 +27,7 @@
   final native boolean getCreateNotes() /*-{ return this.create_notes ? true : false; }-*/;
   final native boolean getCreateNotesAsync() /*-{ return this.create_notes_async ? true : false; }-*/;
   final native JsArrayString getBlockedNames() /*-{ return this.blocked_names; }-*/;
+  final native NativeMap<GroupInfo> getGroups() /*-{ return this.groups; }-*/;
 
   final native void setInfoMessage(String s) /*-{ this.info = s; }-*/;
   final native void setOnSuccessMessage(String s) /*-{ this.on_success = s; }-*/;
@@ -40,10 +42,19 @@
     }
 
   }
-
   final native void initBlockedNames() /*-{ this.blocked_names = []; }-*/;
   final native void addBlockedName(String n) /*-{ this.blocked_names.push(n); }-*/;
 
+  final void setGroups(List<String> groups) {
+    initGroups();
+    for (String g : groups) {
+      addGroup(g);
+    }
+
+  }
+  final native void initGroups() /*-{ this.groups = []; }-*/;
+  final native void addGroup(String g) /*-{ this.groups.push(g); }-*/;
+
   static ConfigInfo create() {
     ConfigInfo g = (ConfigInfo) createObject();
     return g;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java
index 7d2fc98..9b23513 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java
@@ -49,6 +49,7 @@
   private CheckBox createNotesCheckBox;
   private CheckBox createNotesAsyncCheckBox;
   private StringListPanel blockedUsernamesPanel;
+  private StringListPanel groupsPanel;
   private Button saveButton;
 
   ServiceUserSettingsScreen() {
@@ -172,6 +173,11 @@
             info.getBlockedNames(), saveButton);
     add(blockedUsernamesPanel);
 
+    groupsPanel =
+        new StringListPanel("Groups", "Group Name",
+            info.getGroups().keySet(), saveButton);
+    add(groupsPanel);
+
     HorizontalPanel buttons = new HorizontalPanel();
     add(buttons);
 
@@ -197,6 +203,7 @@
       in.setCreateNotesAsync(createNotesAsyncCheckBox.getValue());
     }
     in.setBlockedNames(blockedUsernamesPanel.getValues());
+    in.setGroups(groupsPanel.getValues());
     new RestApi("config").id("server").view(Plugin.get().getPluginName(), "config")
         .put(in, new AsyncCallback<JavaScriptObject>() {
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
index 27ca410..9aeaf6f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
@@ -33,6 +33,7 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 public class StringListPanel extends FlowPanel {
@@ -42,6 +43,11 @@
 
   StringListPanel(String title, String fieldName, JsArrayString values,
       final FocusWidget w) {
+    this(title, fieldName, Natives.asList(values), w);
+  }
+
+  StringListPanel(String title, String fieldName, Collection<String> values,
+      final FocusWidget w) {
     Label titleLabel = new Label(title);
     titleLabel.setStyleName("serviceuser-smallHeading");
     add(titleLabel);
@@ -112,9 +118,9 @@
       setText(0, 1, name);
     }
 
-    void display(JsArrayString values) {
+    void display(Collection<String> values) {
       int row = 1;
-      for (String v : Natives.asList(values)) {
+      for (String v : values) {
         populate(row, v);
         row++;
       }
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 453299b..45388f7 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -622,7 +622,7 @@
 
 Sets the configuration of the @PLUGIN@ plugin.
 
-The new configuration must be specified as a [ConfigInfo](#config-info)
+The new configuration must be specified as a [ConfigInput](#config-input)
 entity in the request body. Not setting a parameter leaves the parameter
 unchanged.
 
@@ -643,7 +643,8 @@
 
 ### <a id="config-info"></a>ConfigInfo
 
-The `ConfigInfo` entity contains configuration of the @PLUGIN@ plugin.
+The `ConfigInfo` entity contains the configuration of the @PLUGIN@
+plugin.
 
 * _info_: HTML formatted message that should be displayed on the
   service user creation screen.
@@ -659,6 +660,33 @@
   `false`).
 * _blocked\_names_: List of usernames which are forbidden to be used as
   name for a service user. The blocked usernames are case insensitive.
+* _groups_: Map of groups to which newly created service users are
+  automatically added. The map maps the group name to a
+  [GroupInfo](../../../Documentation/rest-api-groups.html#group-info)
+  entity. The `name` field in the GroupInfo entities is not set since
+  the names are already available as map keys.
+
+### <a id="config-input"></a>ConfigInput
+
+The `ConfigInput` entity contains updates for the configuration of the
+@PLUGIN@ plugin.
+
+* _info_: HTML formatted message that should be displayed on the
+  service user creation screen.
+* _on\_success_: HTML formatted message that should be displayed after
+  a service user was successfully created.
+* _allow\_email_: Whether it is allowed to provide an email address for
+  a service user (not set if `false`).
+* _create\_notes_: Whether commits of a service user should be
+  annotated by a Git note that contains information about the current
+  owners of the service user (not set if `false`).
+* _create\_notes\_async_: Whether the Git notes on commits that are
+  pushed by a service user should be created asynchronously (not set if
+  `false`).
+* _blocked\_names_: List of usernames which are forbidden to be used as
+  name for a service user. The blocked usernames are case insensitive.
+* _groups_: List of names of internal groups to which newly created
+  service users should be automatically added.
 
 ### <a id="email-input"></a>EmailInput