From 05f229883c4e15e044c5c103acf69265cfb8806e Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 05 Jun 2014 08:38:35 -0400
Subject: [PATCH] Add a basic SSH public key management UI

---
 src/main/java/com/gitblit/manager/IRuntimeManager.java       |   27 +++
 src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java    |  161 ++++++++++++++++++++
 src/main/java/com/gitblit/wicket/pages/UserPage.java         |   22 ++
 src/main/java/com/gitblit/GitBlit.java                       |   15 +
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties    |   10 +
 src/main/java/com/gitblit/wicket/panels/TextAreaOption.java  |   54 ++++++
 src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html    |   46 +++++
 src/main/java/com/gitblit/manager/RuntimeManager.java        |   39 ++++
 src/main/java/com/gitblit/wicket/pages/UserPage.html         |   14 +
 src/main/java/com/gitblit/manager/GitblitManager.java        |   15 +
 src/main/java/com/gitblit/manager/ServicesManager.java       |   18 +
 src/main/java/com/gitblit/wicket/panels/TextAreaOption.html  |   20 ++
 src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java |   15 +
 13 files changed, 449 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 3db5f08..8179385 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -117,6 +117,21 @@
 		return servicesManager.isServingRepositories();
 	}
 
+	@Override
+	public boolean isServingHTTP() {
+		return servicesManager.isServingHTTP();
+	}
+
+	@Override
+	public boolean isServingGIT() {
+		return servicesManager.isServingGIT();
+	}
+
+	@Override
+	public boolean isServingSSH() {
+		return servicesManager.isServingSSH();
+	}
+
 	protected Object [] getModules() {
 		return new Object [] { new GitBlitModule()};
 	}
diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java
index 16c71ba..ef2433d 100644
--- a/src/main/java/com/gitblit/manager/GitblitManager.java
+++ b/src/main/java/com/gitblit/manager/GitblitManager.java
@@ -602,6 +602,21 @@
 	}
 
 	@Override
+	public boolean isServingHTTP() {
+		return runtimeManager.isServingHTTP();
+	}
+
+	@Override
+	public boolean isServingGIT() {
+		return runtimeManager.isServingGIT();
+	}
+
+	@Override
+	public boolean isServingSSH() {
+		return runtimeManager.isServingSSH();
+	}
+
+	@Override
 	public TimeZone getTimezone() {
 		return runtimeManager.getTimezone();
 	}
diff --git a/src/main/java/com/gitblit/manager/IRuntimeManager.java b/src/main/java/com/gitblit/manager/IRuntimeManager.java
index 29e7368..b2d7a2b 100644
--- a/src/main/java/com/gitblit/manager/IRuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/IRuntimeManager.java
@@ -57,6 +57,33 @@
 	boolean isServingRepositories();
 
 	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over HTTP.
+	 *
+	 * @return true if Gitblit is serving repositories over HTTP
+ 	 * @since 1.6.0
+	 */
+	boolean isServingHTTP();
+
+	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over the GIT Daemon protocol.
+	 *
+	 * @return true if Gitblit is serving repositories over the GIT Daemon protocol
+ 	 * @since 1.6.0
+	 */
+	boolean isServingGIT();
+
+	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over the SSH protocol.
+	 *
+	 * @return true if Gitblit is serving repositories over the SSH protocol
+ 	 * @since 1.6.0
+	 */
+	boolean isServingSSH();
+
+	/**
 	 * Determine if this Gitblit instance is running in debug mode
 	 *
 	 * @return true if Gitblit is running in debug mode
diff --git a/src/main/java/com/gitblit/manager/RuntimeManager.java b/src/main/java/com/gitblit/manager/RuntimeManager.java
index 52f4d67..9cdc64e 100644
--- a/src/main/java/com/gitblit/manager/RuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -119,9 +119,42 @@
 	 */
 	@Override
 	public boolean isServingRepositories() {
-		return settings.getBoolean(Keys.git.enableGitServlet, true)
-				|| (settings.getInteger(Keys.git.daemonPort, 0) > 0)
-				|| (settings.getInteger(Keys.git.sshPort, 0) > 0);
+		return isServingHTTP()
+				|| isServingGIT()
+				|| isServingSSH();
+	}
+
+	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over the HTTP protocol.
+	 *
+	 * @return true if Gitblit is serving repositories over the HTTP protocol
+	 */
+	@Override
+	public boolean isServingHTTP() {
+		return settings.getBoolean(Keys.git.enableGitServlet, true);
+	}
+
+	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over the Git Daemon protocol.
+	 *
+	 * @return true if Gitblit is serving repositories over the Git Daemon protocol
+	 */
+	@Override
+	public boolean isServingGIT() {
+		return settings.getInteger(Keys.git.daemonPort, 0) > 0;
+	}
+
+	/**
+	 * Determine if this Gitblit instance is actively serving git repositories
+	 * over the SSH protocol.
+	 *
+	 * @return true if Gitblit is serving repositories over the SSH protocol
+	 */
+	@Override
+	public boolean isServingSSH() {
+		return settings.getInteger(Keys.git.sshPort, 0) > 0;
 	}
 
 	/**
diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java
index 755d8ba..3721578 100644
--- a/src/main/java/com/gitblit/manager/ServicesManager.java
+++ b/src/main/java/com/gitblit/manager/ServicesManager.java
@@ -112,9 +112,21 @@
 	}
 
 	public boolean isServingRepositories() {
-		return settings.getBoolean(Keys.git.enableGitServlet, true)
-				|| (gitDaemon != null && gitDaemon.isRunning())
-				|| (sshDaemon != null && sshDaemon.isRunning());
+		return isServingHTTP()
+				|| isServingGIT()
+				|| isServingSSH();
+	}
+
+	public boolean isServingHTTP() {
+		return settings.getBoolean(Keys.git.enableGitServlet, true);
+	}
+
+	public boolean isServingGIT() {
+		return gitDaemon != null && gitDaemon.isRunning();
+	}
+
+	public boolean isServingSSH() {
+		return sshDaemon != null && sshDaemon.isRunning();
 	}
 
 	protected void configureFederation() {
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 8117179..7dc0f9b 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -729,4 +729,12 @@
 gb.languagePreference = Language Preference
 gb.languagePreferenceDescription = Select your preferred translation for the Gitblit UI
 gb.displayNameDescription = The preferred name for display
-gb.emailAddressDescription = The primary email address for receiving notifications
\ No newline at end of file
+gb.emailAddressDescription = The primary email address for receiving notifications
+gb.sshKeys = SSH Keys
+gb.sshKeysDescription = SSH public key authentication is a secure alternative to password authentication
+gb.addSshKey = Add SSH Key
+gb.key = Key
+gb.comment = Comment
+gb.sshKeyCommentDescription = Enter an optional comment. If blank, the comment will be extracted from the key data.
+gb.permission = Permission
+gb.sshKeyPermissionDescription = Specify the access permission for the SSH key
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.html b/src/main/java/com/gitblit/wicket/pages/UserPage.html
index 0926787..d71cb2b 100644
--- a/src/main/java/com/gitblit/wicket/pages/UserPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.html
@@ -20,6 +20,7 @@
 				<ul class="nav nav-tabs">
 					<li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
 					<div wicket:id="preferencesLink"></div>
+					<div wicket:id="sshKeysLink"></div>
 				</ul>
 	
 				<!-- tab content -->
@@ -37,6 +38,9 @@
 					<!-- preferences tab -->
 					<div wicket:id="preferencesTab"></div>
 					
+					<!-- ssh keys tab -->
+					<div wicket:id="sshKeysTab"></div>
+					
 				</div>
 			</div>
 		</div>
@@ -45,6 +49,10 @@
 
 <wicket:fragment wicket:id="preferencesLinkFragment">
 	<li><a href="#preferences" data-toggle="tab"><wicket:message key="gb.preferences"></wicket:message></a></li>
+</wicket:fragment>
+
+<wicket:fragment wicket:id="sshKeysLinkFragment">
+	<li><a href="#ssh" data-toggle="tab"><wicket:message key="gb.sshKeys"></wicket:message></a></li>
 </wicket:fragment>
 
 <wicket:fragment wicket:id="preferencesTabFragment">
@@ -63,6 +71,12 @@
 	</div>
 </wicket:fragment>
 
+<wicket:fragment wicket:id="sshKeysTabFragment">
+	<div class="tab-pane" id="ssh">
+		<div wicket:id="sshKeysPanel"></div>		
+	</div>
+</wicket:fragment>
+
 </wicket:extend>
 </body>
 </html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.java b/src/main/java/com/gitblit/wicket/pages/UserPage.java
index baad4a0..4a955c7 100644
--- a/src/main/java/com/gitblit/wicket/pages/UserPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -50,6 +50,7 @@
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.ChoiceOption;
 import com.gitblit.wicket.panels.ProjectRepositoryPanel;
+import com.gitblit.wicket.panels.SshKeysPanel;
 import com.gitblit.wicket.panels.TextOption;
 import com.gitblit.wicket.panels.UserTitlePanel;
 
@@ -100,10 +101,22 @@
 
 		if (isMyProfile) {
 			addPreferences(user);
+
+			if (app().gitblit().isServingSSH()) {
+				// show the SSH key management tab
+				addSshKeys(user);
+			} else {
+				// SSH daemon is disabled, hide keys tab
+				add(new Label("sshKeysLink").setVisible(false));
+				add(new Label("sshKeysTab").setVisible(false));
+			}
 		} else {
 			// visiting user
 			add(new Label("preferencesLink").setVisible(false));
 			add(new Label("preferencesTab").setVisible(false));
+
+			add(new Label("sshKeysLink").setVisible(false));
+			add(new Label("sshKeysTab").setVisible(false));
 		}
 
 		List<RepositoryModel> repositories = getRepositories(params);
@@ -251,6 +264,15 @@
 		add(fragment.setRenderBodyOnly(true));
 	}
 
+	private void addSshKeys(final UserModel user) {
+		Fragment keysTab = new Fragment("sshKeysTab", "sshKeysTabFragment", this);
+		keysTab.add(new SshKeysPanel("sshKeysPanel", user, getClass(), getPageParameters()));
+
+		// add the SSH keys tab
+		add(new Fragment("sshKeysLink", "sshKeysLinkFragment", this).setRenderBodyOnly(true));
+		add(keysTab.setRenderBodyOnly(true));
+	}
+
 	private class Language implements Serializable {
 
 		private static final long serialVersionUID = 1L;
diff --git a/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html
new file mode 100644
index 0000000..d67b704
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+	<h4><wicket:message key="gb.sshKeys"></wicket:message></h4>
+	<p><wicket:message key="gb.sshKeysDescription"></wicket:message></p>
+	<hr />
+		
+	<div wicket:id="keys">
+		<div style="display:inline-block;font-size:2em;padding:10px;">
+			<i class="fa fa-key"></i>
+		</div>
+		<div style="display:inline-block;">
+			<div wicket:id="comment" style="font-weight:bold;"></div>
+			<pre wicket:id="fingerprint"></pre>
+		</div>
+		
+		<div style="display:inline-block;padding: 0px 20px">
+			<div wicket:id="permission" style="font-weight:bold;"></div>
+			<div wicket:id="algorithm"></div>
+		</div>
+		
+		<div style="display:inline-block;vertical-align:text-bottom;">
+			<button class="btn btn-danger" wicket:id="delete"><wicket:message key="gb.delete"></wicket:message></button>
+		</div>
+		
+		<hr />
+	</div>
+	
+	<div class="well">
+		<form wicket:id="addKeyForm">
+			<h4><wicket:message key="gb.addSshKey"></wicket:message></h4>
+			<div wicket:id="addKeyData"></div>
+			<div wicket:id="addKeyPermission"></div>
+			<div wicket:id="addKeyComment"></div>
+		
+			<div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addKeyButton" /></div>
+		</form>
+	</div>
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
new file mode 100644
index 0000000..03cb93c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * 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.gitblit.wicket.panels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.SshKey;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+
+
+/**
+ * A panel that enumerates and manages SSH public keys.
+ *
+ * @author James Moger
+ *
+ */
+public class SshKeysPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final UserModel user;
+
+	private final Class<? extends WebPage> pageClass;
+
+	private final PageParameters params;
+
+	public SshKeysPanel(String wicketId, UserModel user, Class<? extends WebPage> pageClass, PageParameters params) {
+		super(wicketId);
+
+		this.user = user;
+		this.pageClass = pageClass;
+		this.params = params;
+	}
+
+	@Override
+	protected void onInitialize() {
+		super.onInitialize();
+		List<SshKey> keys = app().keys().getKeys(user.username);
+
+		final ListDataProvider<SshKey> dp = new ListDataProvider<SshKey>(keys);
+		DataView<SshKey> keysView = new DataView<SshKey>("keys", dp) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void populateItem(final Item<SshKey> item) {
+				final SshKey key = item.getModelObject();
+				item.add(new Label("comment", key.getComment()));
+				item.add(new Label("fingerprint", key.getFingerprint()));
+				item.add(new Label("permission", key.getPermission().toString()));
+				item.add(new Label("algorithm", key.getAlgorithm()));
+
+				Link<Void> delete = new Link<Void>("delete") {
+
+					private static final long serialVersionUID = 1L;
+
+					@Override
+					public void onClick() {
+						if (app().keys().removeKey(user.username, key)) {
+							setRedirect(true);
+							setResponsePage(pageClass, params);
+						}
+					}
+				};
+				item.add(delete);
+			}
+		};
+		add(keysView);
+
+		Form<Void> addKeyForm = new Form<Void>("addKeyForm");
+
+		final IModel<String> keyData = Model.of("");
+		addKeyForm.add(new TextAreaOption("addKeyData",
+				getString("gb.key"),
+				null,
+				"span5",
+				keyData));
+
+		final IModel<AccessPermission> keyPermission = Model.of(AccessPermission.PUSH);
+		addKeyForm.add(new ChoiceOption<AccessPermission>("addKeyPermission",
+				getString("gb.permission"),
+				getString("gb.sshKeyPermissionDescription"),
+				keyPermission,
+				Arrays.asList(AccessPermission.SSHPERMISSIONS)));
+
+		final IModel<String> keyComment = Model.of("");
+		addKeyForm.add(new TextOption("addKeyComment",
+				getString("gb.comment"),
+				getString("gb.sshKeyCommentDescription"),
+				"span5",
+				keyComment));
+
+		addKeyForm.add(new AjaxButton("addKeyButton") {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+
+				UserModel user = GitBlitWebSession.get().getUser();
+				String data = keyData.getObject();
+				if (StringUtils.isEmpty(data)) {
+					// do not submit empty key
+					return;
+				}
+
+				SshKey key = new SshKey(data);
+				try {
+					key.getPublicKey();
+				} catch (Exception e) {
+					// failed to parse the key
+					return;
+				}
+
+				AccessPermission permission = keyPermission.getObject();
+				key.setPermission(permission);
+
+				String comment  = keyComment.getObject();
+				if (!StringUtils.isEmpty(comment)) {
+					key.setComment(comment);
+				}
+
+				if (app().keys().addKey(user.username, key)) {
+					setRedirect(true);
+					setResponsePage(pageClass, params);
+				}
+			}
+		});
+
+		add(addKeyForm);
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/TextAreaOption.html b/src/main/java/com/gitblit/wicket/panels/TextAreaOption.html
new file mode 100644
index 0000000..bb7dc7c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TextAreaOption.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+	<div style="padding-top:4px;">
+		<div style="margin-bottom:1px;">
+			<b><span wicket:id="name"></span></b>
+		</div>
+		<label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
+		<p style="padding-top:5px;"><textarea rows="12" class="span5" wicket:id="text"></textarea></p>
+		</label>
+		
+	</div>
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/TextAreaOption.java b/src/main/java/com/gitblit/wicket/panels/TextAreaOption.java
new file mode 100644
index 0000000..d2c74a0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TextAreaOption.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * 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.gitblit.wicket.panels;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.TextArea;
+import org.apache.wicket.model.IModel;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+/**
+ * A re-usable textarea option panel.
+ *
+ * title
+ *     description
+ *     [text
+ *           area]
+ *
+ * @author James Moger
+ *
+ */
+public class TextAreaOption extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public TextAreaOption(String wicketId, String title, String description, IModel<String> model) {
+		this(wicketId, title, description, null, model);
+	}
+
+	public TextAreaOption(String wicketId, String title, String description, String css, IModel<String> model) {
+		super(wicketId);
+		add(new Label("name", title));
+		add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
+		TextArea<String> tf = new TextArea<String>("text", model);
+		if (!StringUtils.isEmpty(css)) {
+			WicketUtils.setCssClass(tf, css);
+		}
+		add(tf);
+	}
+}
diff --git a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
index 6e56a87..54be539 100644
--- a/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
+++ b/src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
@@ -82,6 +82,21 @@
 	}
 
 	@Override
+	public boolean isServingHTTP() {
+		return true;
+	}
+
+	@Override
+	public boolean isServingGIT() {
+		return true;
+	}
+
+	@Override
+	public boolean isServingSSH() {
+		return true;
+	}
+
+	@Override
 	public boolean isDebugMode() {
 		return true;
 	}

--
Gitblit v1.9.1