Merge "Only accept pushes from service users if at least one owner is active"
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 43afe00..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,31 +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;
   }
 
@@ -48,10 +85,12 @@
   }
 
   public class ConfigInfo {
-    String info;
-    String onSuccess;
-    Boolean allowEmail;
-    Boolean createNotes;
-    Boolean createNotesAsync;
+    public String info;
+    public String onSuccess;
+    public Boolean allowEmail;
+    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 524afb8..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;
@@ -33,6 +36,7 @@
 import org.eclipse.jgit.util.FS;
 
 import java.io.IOException;
+import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 public class PutConfig implements RestModifyView<ConfigResource, Input>{
@@ -42,23 +46,28 @@
     public Boolean allowEmail;
     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();
@@ -79,6 +88,18 @@
     if (input.createNotesAsync != null) {
       setBoolean(cfg, "createNotesAsync", input.createNotesAsync);
     }
+    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 ae81ca4..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,7 +14,11 @@
 
 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;
+
+import java.util.List;
 
 public class ConfigInfo extends JavaScriptObject {
   final native String getInfoMessage() /*-{ return this.info }-*/;
@@ -22,6 +26,8 @@
   final native boolean getAllowEmail() /*-{ return this.allow_email ? true : false; }-*/;
   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; }-*/;
@@ -29,6 +35,26 @@
   final native void setCreateNotes(boolean s) /*-{ this.create_notes = s; }-*/;
   final native void setCreateNotesAsync(boolean s) /*-{ this.create_notes_async = s; }-*/;
 
+  final void setBlockedNames(List<String> blockedNames) {
+    initBlockedNames();
+    for (String n : blockedNames) {
+      addBlockedName(n);
+    }
+
+  }
+  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/EditableValue.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java
index d48b327..4e4a79a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java
@@ -60,6 +60,7 @@
         label.setVisible(false);
         edit.setVisible(false);
         input.setVisible(true);
+        input.setFocus(true);
         save.setVisible(true);
         if (warning != null) {
           warning.setVisible(true);
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 d142154..12afb28 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
@@ -48,6 +48,8 @@
   private CheckBox allowEmailCheckBox;
   private CheckBox createNotesCheckBox;
   private CheckBox createNotesAsyncCheckBox;
+  private StringListPanel blockedUsernamesPanel;
+  private StringListPanel groupsPanel;
   private Button saveButton;
 
   ServiceUserSettingsScreen() {
@@ -157,9 +159,6 @@
       }
     });
 
-    HorizontalPanel buttons = new HorizontalPanel();
-    add(buttons);
-
     saveButton = new Button("Save");
     saveButton.addStyleName("serviceuser-saveButton");
     saveButton.addClickHandler(new ClickHandler() {
@@ -168,6 +167,25 @@
         doSave();
       }
     });
+
+    blockedUsernamesPanel =
+        new StringListPanel("Blocked Usernames", "Username",
+            info.getBlockedNames(), saveButton);
+    blockedUsernamesPanel.setInfo("List of usernames which are "
+        + "forbidden to be used as name for a service user. "
+        + "The blocked usernames are case insensitive.");
+    add(blockedUsernamesPanel);
+
+    groupsPanel =
+        new StringListPanel("Groups", "Group Name",
+            info.getGroups().keySet(), saveButton);
+    groupsPanel.setInfo("Names of groups to which newly created "
+        + "service users should be added automatically.");
+    add(groupsPanel);
+
+    HorizontalPanel buttons = new HorizontalPanel();
+    add(buttons);
+
     buttons.add(saveButton);
     saveButton.setEnabled(false);
     OnEditEnabler onEditEnabler = new OnEditEnabler(saveButton, infoMsgTxt);
@@ -189,6 +207,8 @@
     if (createNotesAsyncCheckBox.isEnabled()) {
       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
new file mode 100644
index 0000000..2a2f970
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
@@ -0,0 +1,201 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.serviceuser.client;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class StringListPanel extends FlowPanel {
+  private final NpTextBox input;
+  private final StringListTable t;
+  private final Button deleteButton;
+  private final HorizontalPanel titlePanel;
+  private Image info;
+
+  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) {
+    titlePanel = new HorizontalPanel();
+    Label titleLabel = new Label(title);
+    titleLabel.setStyleName("serviceuser-smallHeading");
+    titlePanel.add(titleLabel);
+    add(titlePanel);
+    input = new NpTextBox();
+    input.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          w.setEnabled(true);
+          add();
+        }
+      }
+    });
+    HorizontalPanel p = new HorizontalPanel();
+    p.add(input);
+    Button addButton = new Button("Add");
+    addButton.setEnabled(false);
+    new OnEditEnabler(addButton, input);
+    addButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        w.setEnabled(true);
+        add();
+      }
+    });
+    p.add(addButton);
+    add(p);
+
+    t = new StringListTable(fieldName);
+    add(t);
+
+    deleteButton = new Button("Delete");
+    deleteButton.setEnabled(false);
+    add(deleteButton);
+    deleteButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        w.setEnabled(true);
+        t.deleteChecked();
+      }
+    });
+
+    t.display(values);
+  }
+
+  void setInfo(String msg) {
+    if (info == null) {
+      info = new Image(ServiceUserPlugin.RESOURCES.info());
+      titlePanel.add(info);
+    }
+    info.setTitle(msg);
+  }
+
+  List<String> getValues() {
+    return t.getValues();
+  }
+
+  private void add() {
+    String v = input.getValue().trim();
+    if (!v.isEmpty()) {
+      input.setValue("");
+      t.insert(v);
+    }
+  }
+
+  private class StringListTable extends FlexTable {
+    StringListTable(String name) {
+      setStyleName("serviceuser-stringListTable");
+      FlexCellFormatter fmt = getFlexCellFormatter();
+      fmt.addStyleName(0, 0, "iconHeader");
+      fmt.addStyleName(0, 0, "topMostCell");
+      fmt.addStyleName(0, 0, "leftMostCell");
+      fmt.addStyleName(0, 1, "dataHeader");
+      fmt.addStyleName(0, 1, "topMostCell");
+
+      setText(0, 1, name);
+    }
+
+    void display(Collection<String> values) {
+      int row = 1;
+      for (String v : values) {
+        populate(row, v);
+        row++;
+      }
+    }
+
+    List<String> getValues() {
+      List<String> values = new ArrayList<String>();
+      for (int row = 1; row < getRowCount(); row++) {
+        values.add(getText(row, 1));
+      }
+      return values;
+    }
+
+    private void populate(int row, String value) {
+      FlexCellFormatter fmt = getFlexCellFormatter();
+      fmt.addStyleName(row, 0, "leftMostCell");
+      fmt.addStyleName(row, 0, "iconCell");
+      fmt.addStyleName(row, 1, "dataCell");
+
+      CheckBox checkBox = new CheckBox();
+      checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          enableDelete();
+        }
+      });
+      setWidget(row, 0, checkBox);
+      setText(row, 1, value);
+    }
+
+    void insert(String v) {
+      int insertPos = getRowCount();
+      for (int row = 1; row < getRowCount(); row++) {
+        int compareResult = v.compareTo(getText(row, 1));
+        if (compareResult < 0)  {
+          insertPos = row;
+          break;
+        } else if (compareResult == 0) {
+          return;
+        }
+      }
+      insertRow(insertPos);
+      populate(insertPos, v);
+    }
+
+    void enableDelete() {
+      for (int row = 1; row < getRowCount(); row++) {
+        if (((CheckBox) getWidget(row, 0)).getValue()) {
+          deleteButton.setEnabled(true);
+          return;
+        }
+      }
+      deleteButton.setEnabled(false);
+    }
+
+    void deleteChecked() {
+      deleteButton.setEnabled(false);
+      for (int row = 1; row < getRowCount(); row++) {
+        if (((CheckBox) getWidget(row, 0)).getValue()) {
+          removeRow(row--);
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
index 7f70168..807b1be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
@@ -7,23 +7,27 @@
 }
 
 .serviceuser-serviceUserTable,
-.serviceuser-sshKeyTable {
+.serviceuser-sshKeyTable,
+.serviceuser-stringListTable {
   border-collapse: separate;
   border-spacing: 0;
 }
 
 .serviceuser-serviceUserTable .leftMostCell,
-.serviceuser-sshKeyTable .leftMostCell {
+.serviceuser-sshKeyTable .leftMostCell,
+.serviceuser-stringListTable .leftMostCell {
   border-left: 1px solid #EEE;
 }
 
 .serviceuser-serviceUserTable .topMostCell,
-.serviceuser-sshKeyTable .topMostCell {
+.serviceuser-sshKeyTable .topMostCell,
+.serviceuser-stringListTable .topMostCell {
   border-top: 1px solid #EEE;
 }
 
 .serviceuser-serviceUserTable .dataHeader,
-.serviceuser-sshKeyTable .dataHeader {
+.serviceuser-sshKeyTable .dataHeader,
+.serviceuser-stringListTable .dataHeader {
   border: 1px solid #FFF;
   padding: 2px 6px 1px;
   background-color: #EEE;
@@ -32,14 +36,16 @@
   color: textColor;
 }
 
-.serviceuser-sshKeyTable .iconHeader {
+.serviceuser-sshKeyTable .iconHeader,
+.serviceuser-stringListTable .iconHeader {
   border-top: 1px solid #FFF;
   border-bottom: 1px solid #FFF;
   background-color: #EEE;
 }
 
 .serviceuser-serviceUserTable .dataCell,
-.serviceuser-sshKeyTable .dataCell {
+.serviceuser-sshKeyTable .dataCell,
+.serviceuser-stringListTable .dataCell {
   padding-left: 5px;
   padding-right: 5px;
   border-right: 1px solid #EEE;
@@ -48,7 +54,8 @@
   height: 20px;
 }
 
-.serviceuser-sshKeyTable .iconCell {
+.serviceuser-sshKeyTable .iconCell,
+.serviceuser-stringListTable .iconCell {
   width: 1px;
   padding: 0px;
   vertical-align: middle;
@@ -124,3 +131,8 @@
   background: #BFC;
   border-style: inset;
 }
+
+.serviceuser-smallHeading {
+  margin-top: 5px;
+  font-weight: bold;
+}
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index f06090a..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.
@@ -657,6 +658,35 @@
 * _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_: 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