Allow to see/add/delete SSH keys from service user screen
Change-Id: I59173bf474e3cb0022b21cb6372cebda266ec482
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/pom.xml b/pom.xml
index b6cc2d9..06d8d1b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,19 @@
<version>2.5.1</version>
<scope>provided</scope>
</dependency>
+
+ <dependency>
+ <groupId>gwtexpui</groupId>
+ <artifactId>gwtexpui</artifactId>
+ <version>1.3.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>gwtexpui</groupId>
+ <artifactId>gwtexpui</artifactId>
+ <version>1.3.4</version>
+ <classifier>sources</classifier>
+ </dependency>
</dependencies>
<repositories>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
index 01f86d2..9d921cb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
@@ -21,6 +21,8 @@
<inherits name="com.google.gerrit.Plugin"/>
<inherits name="com.google.gwt.http.HTTP"/>
<inherits name="com.google.gwt.json.JSON"/>
+ <inherits name='com.google.gwtexpui.clippy.Clippy'/>
+ <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
<!-- Using GWT built-in themes adds a number of static -->
<!-- resources to the plugin. No theme inherits lines were -->
<!-- added in order to make this plugin as simple as possible -->
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
index 5242c60..faacb3c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
@@ -28,7 +28,6 @@
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
@@ -85,27 +84,7 @@
Panel sshKeyPanel = new VerticalPanel();
sshKeyPanel.add(new Label("Public SSH Key:"));
- DisclosurePanel dp = new DisclosurePanel("How to generate an SSH Key");
- StringBuilder b = new StringBuilder();
- b.append("<ol>")
- .append("<li>From the Terminal or Git Bash, run <em>ssh-keygen</em></li>")
- .append("<li>")
- .append("Enter a path for the key, e.g. <em>id_rsa</em>. If you are generating the key<br />")
- .append("on your local system take care to not overwrite your own SSH key.")
- .append("</li>")
- .append("<li>")
- .append("Enter a passphrase only if the service where you intend to use this<br />")
- .append("service user is able to deal with passphrases, otherwise leave it blank.<br />")
- .append("Remember this passphrase, as you will need it to unlock the key.")
- .append("</li>")
- .append("<li>")
- .append("Open <em>id_rsa.pub</em> and copy & paste the contents into the box below.<br />")
- .append("Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />")
- .append("while <em>id_rsa</em> is your private key and should be kept secret.")
- .append("</li>")
- .append("</ol>");
- dp.add(new HTML(b.toString()));
- sshKeyPanel.add(dp);
+ sshKeyPanel.add(new SshKeyHelpPanel());
sshKeyTxt = new TextArea();
sshKeyTxt.addKeyPressHandler(new KeyPressHandler() {
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
index 534063c..cd02659 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
@@ -56,6 +56,8 @@
t.addRow("Created By", info.created_by());
t.addRow("Created At", info.created_at());
add(t);
+
+ add(new SshPanel(info.username()));
}
private static class MyTable extends FlexTable {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
new file mode 100644
index 0000000..134b110
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
@@ -0,0 +1,46 @@
+// 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.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+
+public class SshKeyHelpPanel extends FlowPanel {
+
+ SshKeyHelpPanel() {
+ DisclosurePanel dp = new DisclosurePanel("How to generate an SSH Key");
+ StringBuilder b = new StringBuilder();
+ b.append("<ol>")
+ .append("<li>From the Terminal or Git Bash, run <em>ssh-keygen</em></li>")
+ .append("<li>")
+ .append("Enter a path for the key, e.g. <em>id_rsa</em>. If you are generating the key<br />")
+ .append("on your local system take care to not overwrite your own SSH key.")
+ .append("</li>")
+ .append("<li>")
+ .append("Enter a passphrase only if the service where you intend to use this<br />")
+ .append("service user is able to deal with passphrases, otherwise leave it blank.<br />")
+ .append("Remember this passphrase, as you will need it to unlock the key.")
+ .append("</li>")
+ .append("<li>")
+ .append("Open <em>id_rsa.pub</em> and copy & paste the contents into the box below.<br />")
+ .append("Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />")
+ .append("while <em>id_rsa</em> is your private key and should be kept secret.")
+ .append("</li>")
+ .append("</ol>");
+ dp.add(new HTML(b.toString()));
+ add(dp);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
new file mode 100644
index 0000000..238e548
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
@@ -0,0 +1,29 @@
+// 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.gwt.core.client.JavaScriptObject;
+
+public class SshKeyInfo extends JavaScriptObject {
+ public final native int seq() /*-{ return this.seq || 0; }-*/;
+ public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
+ public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
+ public final native String algorithm() /*-{ return this.algorithm; }-*/;
+ public final native String comment() /*-{ return this.comment; }-*/;
+ public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
+
+ protected SshKeyInfo() {
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
new file mode 100644
index 0000000..bad75b8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
@@ -0,0 +1,364 @@
+// 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.plugin.client.Plugin;
+import com.google.gerrit.plugin.client.rpc.Natives;
+import com.google.gerrit.plugin.client.rpc.NoContent;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class SshPanel extends Composite {
+ private final String serviceUser;
+
+ private SshKeyTable keys;
+
+ private Button showAddKeyBlock;
+ private Panel addKeyBlock;
+ private Button closeAddKeyBlock;
+ private Button clearNew;
+ private Button addNew;
+ private NpTextArea addTxt;
+ private Button deleteKey;
+
+ private Panel serverKeys;
+
+ private int loadCount;
+
+ SshPanel(String serviceUser) {
+ this.serviceUser = serviceUser;
+
+ FlowPanel body = new FlowPanel();
+
+ showAddKeyBlock = new Button("Add Key ...");
+ showAddKeyBlock.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ showAddKeyBlock(true);
+ }
+ });
+
+ keys = new SshKeyTable();
+ body.add(keys);
+ {
+ final FlowPanel fp = new FlowPanel();
+ deleteKey = new Button("Delete");
+ deleteKey.setEnabled(false);
+ deleteKey.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ keys.deleteChecked();
+ }
+ });
+ fp.add(deleteKey);
+ fp.add(showAddKeyBlock);
+ body.add(fp);
+ }
+
+ addKeyBlock = new VerticalPanel();
+ addKeyBlock.setVisible(false);
+ addKeyBlock.setStyleName("serviceuser-addSshKeyPanel");
+ addKeyBlock.add(new Label("Add SSH Public Key"));
+ addKeyBlock.add(new SshKeyHelpPanel());
+
+ addTxt = new NpTextArea();
+ addTxt.setVisibleLines(12);
+ addTxt.setCharacterWidth(80);
+ addTxt.setSpellCheck(false);
+ addKeyBlock.add(addTxt);
+
+ final HorizontalPanel buttons = new HorizontalPanel();
+ addKeyBlock.add(buttons);
+
+ clearNew = new Button("Clear");
+ clearNew.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ addTxt.setText("");
+ addTxt.setFocus(true);
+ }
+ });
+ buttons.add(clearNew);
+
+ addNew = new Button("Add");
+ addNew.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ doAddNew();
+ }
+ });
+ buttons.add(addNew);
+
+ closeAddKeyBlock = new Button("Close");
+ closeAddKeyBlock.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ showAddKeyBlock(false);
+ }
+ });
+ buttons.add(closeAddKeyBlock);
+ buttons.setCellWidth(closeAddKeyBlock, "100%");
+ buttons.setCellHorizontalAlignment(closeAddKeyBlock,
+ HasHorizontalAlignment.ALIGN_RIGHT);
+
+ body.add(addKeyBlock);
+
+ serverKeys = new FlowPanel();
+ body.add(serverKeys);
+
+ initWidget(body);
+ }
+
+ void setKeyTableVisible(final boolean on) {
+ keys.setVisible(on);
+ deleteKey.setVisible(on);
+ closeAddKeyBlock.setVisible(on);
+ }
+
+ void doAddNew() {
+ final String txt = addTxt.getText();
+ if (txt != null && txt.length() > 0) {
+ new RestApi("config").id("server")
+ .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+ .view("sshkeys").post(txt, new AsyncCallback<SshKeyInfo>() {
+ public void onSuccess(SshKeyInfo k) {
+ addTxt.setText("");
+ keys.addOneKey(k);
+ if (!keys.isVisible()) {
+ showAddKeyBlock(false);
+ setKeyTableVisible(true);
+ keys.updateDeleteButton();
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable caught) {
+ // never invoked
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ refreshSshKeys();
+ }
+
+ private void refreshSshKeys() {
+ new RestApi("config").id("server")
+ .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+ .view("sshkeys").get(new AsyncCallback<JsArray<SshKeyInfo>>() {
+ @Override
+ public void onSuccess(JsArray<SshKeyInfo> result) {
+ keys.display(Natives.asList(result));
+ if (result.length() == 0 && keys.isVisible()) {
+ showAddKeyBlock(true);
+ }
+ if (++loadCount == 2) {
+ display();
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ // never invoked
+ }
+ });
+ }
+
+ void display() {
+ }
+
+ private void showAddKeyBlock(boolean show) {
+ showAddKeyBlock.setVisible(!show);
+ addKeyBlock.setVisible(show);
+ }
+
+ private class SshKeyTable extends FlexTable {
+ private final Map<Integer, SshKeyInfo> sshKeyInfos;
+ private ValueChangeHandler<Boolean> updateDeleteHandler;
+
+ SshKeyTable() {
+ this.sshKeyInfos = new HashMap<Integer, SshKeyInfo>();
+ setStyleName("serviceuser-sshKeyTable");
+ setWidth("");
+ setText(0, 2, "Status");
+ setText(0, 3, "Algorithm");
+ setText(0, 4, "Key");
+ setText(0, 5, "Comment");
+
+ FlexCellFormatter fmt = getFlexCellFormatter();
+ fmt.addStyleName(0, 1, "iconHeader");
+ fmt.addStyleName(0, 2, "dataHeader");
+ fmt.addStyleName(0, 3, "dataHeader");
+ fmt.addStyleName(0, 4, "dataHeader");
+ fmt.addStyleName(0, 5, "dataHeader");
+
+ fmt.addStyleName(0, 1, "topMostCell");
+ fmt.addStyleName(0, 2, "topMostCell");
+ fmt.addStyleName(0, 3, "topMostCell");
+ fmt.addStyleName(0, 4, "topMostCell");
+ fmt.addStyleName(0, 5, "topMostCell");
+
+ updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ updateDeleteButton();
+ }
+ };
+ }
+
+ void deleteChecked() {
+ final HashSet<Integer> sequenceNumbers = new HashSet<Integer>();
+ for (int row = 1; row < getRowCount(); row++) {
+ SshKeyInfo k = getRowItem(row);
+ if (k != null && ((CheckBox) getWidget(row, 1)).getValue()) {
+ sequenceNumbers.add(k.seq());
+ }
+ }
+ if (sequenceNumbers.isEmpty()) {
+ updateDeleteButton();
+ } else {
+ for (int seq : sequenceNumbers) {
+ new RestApi("config").id("server")
+ .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+ .view("sshkeys").id(seq).delete(new AsyncCallback<NoContent>() {
+ public void onSuccess(NoContent result) {
+ for (int row = 1; row < getRowCount();) {
+ SshKeyInfo k = getRowItem(row);
+ if (k != null && sequenceNumbers.contains(k.seq())) {
+ removeRow(row);
+ } else {
+ row++;
+ }
+ }
+ if (getRowCount() == 1) {
+ display(Collections.<SshKeyInfo> emptyList());
+ } else {
+ updateDeleteButton();
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ // never invoked
+ }
+ });
+ }
+ }
+ }
+
+ void display(List<SshKeyInfo> result) {
+ if (result.isEmpty()) {
+ setKeyTableVisible(false);
+ showAddKeyBlock(true);
+ } else {
+ while (1 < getRowCount())
+ removeRow(getRowCount() - 1);
+ for (SshKeyInfo k : result) {
+ addOneKey(k);
+ }
+ setKeyTableVisible(true);
+ deleteKey.setEnabled(false);
+ }
+ }
+
+ void addOneKey(SshKeyInfo k) {
+ FlexCellFormatter fmt = getFlexCellFormatter();
+ int row = getRowCount();
+ insertRow(row);
+ getCellFormatter().addStyleName(row, 0, "iconCell");
+ getCellFormatter().addStyleName(row, 0, "leftMostCell");
+
+ CheckBox sel = new CheckBox();
+ sel.addValueChangeHandler(updateDeleteHandler);
+
+ setWidget(row, 1, sel);
+ if (k.isValid()) {
+ setText(row, 2, "");
+ fmt.removeStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
+ } else {
+ setText(row, 2, "Invalid Key");
+ fmt.addStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
+ }
+ setText(row, 3, k.algorithm());
+
+ CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
+ keyLabel.setPreviewText(elide(k.encodedKey(), 40));
+ setWidget(row, 4, keyLabel);
+
+ setText(row, 5, k.comment());
+
+ fmt.addStyleName(row, 1, "iconCell");
+ fmt.addStyleName(row, 4, "serviceuser-sshKeyPanelEncodedKey");
+ for (int c = 2; c <= 5; c++) {
+ fmt.addStyleName(row, c, "dataCell");
+ }
+
+ setRowItem(row, k);
+ }
+
+ void updateDeleteButton() {
+ boolean on = false;
+ for (int row = 1; row < getRowCount(); row++) {
+ CheckBox sel = (CheckBox) getWidget(row, 1);
+ if (sel.getValue()) {
+ on = true;
+ break;
+ }
+ }
+ deleteKey.setEnabled(on);
+ }
+
+ private SshKeyInfo getRowItem(int row) {
+ return sshKeyInfos.get(row);
+ }
+
+ private void setRowItem(int row, SshKeyInfo sshKeyInfo) {
+ sshKeyInfos.put(row, sshKeyInfo);
+ }
+ }
+
+ static String elide(String s, int len) {
+ if (s == null || s.length() < len || len <= 10) {
+ return s;
+ }
+ return s.substring(0, len - 10) + "..." + s.substring(s.length() - 10);
+ }
+}
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 a094130..6959b1b 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
@@ -6,20 +6,24 @@
margin-left: 10px !important;
}
-.serviceuser-serviceUserTable {
+.serviceuser-serviceUserTable,
+.serviceuser-sshKeyTable {
border-collapse: separate;
border-spacing: 0;
}
-.serviceuser-serviceUserTable .leftMostCell {
+.serviceuser-serviceUserTable .leftMostCell,
+.serviceuser-sshKeyTable .leftMostCell {
border-left: 1px solid #EEE;
}
-.serviceuser-serviceUserTable .topMostCell {
+.serviceuser-serviceUserTable .topMostCell,
+.serviceuser-sshKeyTable .topMostCell {
border-top: 1px solid #EEE;
}
-.serviceuser-serviceUserTable .dataHeader {
+.serviceuser-serviceUserTable .dataHeader,
+.serviceuser-sshKeyTable .dataHeader {
border: 1px solid #FFF;
padding: 2px 6px 1px;
background-color: #EEE;
@@ -28,7 +32,14 @@
color: textColor;
}
-.serviceuser-serviceUserTable .dataCell {
+.serviceuser-sshKeyTable .iconHeader {
+ border-top: 1px solid #FFF;
+ border-bottom: 1px solid #FFF;
+ background-color: #EEE;
+}
+
+.serviceuser-serviceUserTable .dataCell,
+.serviceuser-sshKeyTable .dataCell {
padding-left: 5px;
padding-right: 5px;
border-right: 1px solid #EEE;
@@ -37,3 +48,29 @@
height: 20px;
}
+.serviceuser-sshKeyTable .iconCell {
+ width: 1px;
+ padding: 0px;
+ vertical-align: middle;
+ border-bottom: 1px solid #EEE;
+}
+
+.serviceuser-sshKeyPanelEncodedKey {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-family: mono-font;
+ font-size: small;
+}
+
+.serviceuser-sshKeyPanelInvalid {
+ white-space: nowrap;
+ color: red;
+ font-weight: bold;
+}
+
+.serviceuser-addSshKeyPanel {
+ margin-top: 10px;
+ background-color: #EEE;
+ padding: 5px 5px 5px 5px;
+}