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