From 521cb6022a9ee30bf3115a8dcb991aa5c7e420e3 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 19:01:30 -0400
Subject: [PATCH] Unit tests for ssh daemon and keys dispatcher

---
 src/main/java/com/gitblit/transport/ssh/SshKey.java                        |    4 
 src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java                 |  115 +++++++++++
 src/test/java/com/gitblit/tests/SshUnitTest.java                           |  141 ++++++++++++++
 src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java              |   20 +
 src/test/java/com/gitblit/tests/SshDaemonTest.java                         |  128 +++++-------
 src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java             |    7 
 src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java |   27 +-
 src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java           |   52 ++---
 /dev/null                                                                  |   74 -------
 src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java       |    4 
 src/test/java/com/gitblit/tests/GitBlitSuite.java                          |   18 +
 src/main/java/com/gitblit/transport/ssh/FileKeyManager.java                |    4 
 src/main/java/log4j.properties                                             |    4 
 13 files changed, 385 insertions(+), 213 deletions(-)

diff --git a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java
index 48e5aa2..4ce26d0 100644
--- a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java
+++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java
@@ -38,8 +38,7 @@
  * @author Eric Myrhe
  *
  */
-public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator,
-		SessionListener {
+public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, SessionListener {
 
 	protected final Logger log = LoggerFactory.getLogger(getClass());
 
@@ -47,18 +46,15 @@
 
 	protected final IAuthenticationManager authManager;
 
-	private final Map<ServerSession, Map<PublicKey, Boolean>> cache =
-			new ConcurrentHashMap<ServerSession, Map<PublicKey, Boolean>>();
+	private final Map<ServerSession, Map<PublicKey, Boolean>> cache = new ConcurrentHashMap<ServerSession, Map<PublicKey, Boolean>>();
 
-	public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager,
-			IAuthenticationManager authManager) {
+	public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) {
 		this.keyManager = keyManager;
 		this.authManager = authManager;
 	}
 
 	@Override
-	public boolean authenticate(String username, PublicKey key,
-			ServerSession session) {
+	public boolean authenticate(String username, PublicKey key, ServerSession session) {
 		Map<PublicKey, Boolean> map = cache.get(session);
 		if (map == null) {
 			map = new HashMap<PublicKey, Boolean>();
@@ -73,19 +69,21 @@
 		return result;
 	}
 
-	private boolean doAuthenticate(String username, PublicKey suppliedKey,
-			ServerSession session) {
+	private boolean doAuthenticate(String username, PublicKey suppliedKey, ServerSession session) {
 		SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
 		Preconditions.checkState(client.getUser() == null);
 		username = username.toLowerCase(Locale.US);
 		List<SshKey> keys = keyManager.getKeys(username);
-		if (keys == null || keys.isEmpty()) {
-			log.info("{} has not added any public keys for ssh authentication",
-					username);
+		if (keys.isEmpty()) {
+			log.info("{} has not added any public keys for ssh authentication", username);
 			return false;
 		}
 
+		SshKey pk = new SshKey(suppliedKey);
+		log.debug("auth supplied {}", pk.getFingerprint());
+
 		for (SshKey key : keys) {
+			log.debug("auth compare to {}", key.getFingerprint());
 			if (key.equals(suppliedKey)) {
 				UserModel user = authManager.authenticate(username, key);
 				if (user != null) {
@@ -96,8 +94,7 @@
 			}
 		}
 
-		log.warn("could not authenticate {} for SSH using the supplied public key",
-				username);
+		log.warn("could not authenticate {} for SSH using the supplied public key", username);
 		return false;
 	}
 
diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
index ae4588a..a063dc7 100644
--- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
@@ -90,7 +90,7 @@
 	@Override
 	protected List<SshKey> getKeysImpl(String username) {
 		try {
-			log.info("loading keystore for {}", username);
+			log.info("loading ssh keystore for {}", username);
 			File keystore = getKeystore(username);
 			if (!keystore.exists()) {
 				return null;
@@ -128,7 +128,7 @@
 				return list;
 			}
 		} catch (IOException e) {
-			throw new RuntimeException("Canot read ssh keys", e);
+			throw new RuntimeException("Cannot read ssh keys", e);
 		}
 		return null;
 	}
diff --git a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
index 956a76e..0dbee63 100644
--- a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
@@ -16,6 +16,7 @@
 package com.gitblit.transport.ssh;
 
 import java.text.MessageFormat;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -46,7 +47,11 @@
 			.build(new CacheLoader<String, List<SshKey>>() {
 				@Override
 				public List<SshKey> load(String username) {
-					return getKeysImpl(username);
+					List<SshKey> keys = getKeysImpl(username);
+					if (keys == null) {
+						return Collections.emptyList();
+					}
+					return Collections.unmodifiableList(keys);
 				}
 			});
 
diff --git a/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java
index 18f9a4e..357b34a 100644
--- a/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java
@@ -28,7 +28,7 @@
  */
 public class MemoryKeyManager extends IPublicKeyManager {
 
-	Map<String, List<SshKey>> keys;
+	final Map<String, List<SshKey>> keys;
 
 	public MemoryKeyManager() {
 		keys = new HashMap<String, List<SshKey>>();
@@ -57,7 +57,8 @@
 
 	@Override
 	protected boolean isStale(String username) {
-		return false;
+		// always return true so we gets keys from our hashmap
+		return true;
 	}
 
 	@Override
@@ -75,6 +76,7 @@
 		if (!keys.containsKey(id)) {
 			keys.put(id, new ArrayList<SshKey>());
 		}
+		log.info("added {} key {}", username, key.getFingerprint());
 		return keys.get(id).add(key);
 	}
 
@@ -82,15 +84,27 @@
 	public boolean removeKey(String username, SshKey key) {
 		String id = username.toLowerCase();
 		if (!keys.containsKey(id)) {
+			log.info("can't remove keys for {}", username);
 			return false;
 		}
-		return keys.get(id).remove(key);
+		List<SshKey> list = keys.get(id);
+		boolean success = list.remove(key);
+		if (success) {
+			log.info("removed {} key {}", username, key.getFingerprint());
+		}
+
+		if (list.isEmpty()) {
+			keys.remove(id);
+			log.info("no {} keys left, removed {}", username, username);
+		}
+		return success;
 	}
 
 	@Override
 	public boolean removeAllKeys(String username) {
 		String id = username.toLowerCase();
 		keys.remove(id.toLowerCase());
+		log.info("removed all keys for {}", username);
 		return true;
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKey.java b/src/main/java/com/gitblit/transport/ssh/SshKey.java
index 6ac0cdc..6a20d7d 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshKey.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshKey.java
@@ -155,8 +155,8 @@
 				final byte [] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1]));
 				hash = StringUtils.getMD5(bin);
 			} else {
-				// TODO get hash from publickey
-				hash = "todo";
+				// TODO calculate the correct hash from a PublicKey instance
+				hash = StringUtils.getMD5(getPublicKey().getEncoded());
 			}
 			for (int i = 0; i < hash.length(); i += 2) {
 				sb.append(hash.charAt(i)).append(hash.charAt(i + 1)).append(':');
diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
index dd3c139..0c018f0 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
@@ -41,7 +41,7 @@
 
 	@Override
 	protected AbstractSession createSession(final IoSession io) throws Exception {
-		log.info("connection accepted on " + io);
+		log.info("creating ssh session from {}", io.getRemoteAddress());
 
 		if (io instanceof MinaSession) {
 			if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) {
@@ -59,7 +59,7 @@
 		session.addCloseSessionListener(new SshFutureListener<CloseFuture>() {
 			@Override
 			public void operationComplete(CloseFuture future) {
-				log.info("connection closed on " + io);
+				log.info("closed ssh session from {}", io.getRemoteAddress());
 			}
 		});
 		return session;
diff --git a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
index 62daec6..3f58146 100644
--- a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
@@ -61,7 +61,7 @@
 
 		protected final Logger log = LoggerFactory.getLogger(getClass());
 
-		@Argument(metaVar = "<KEY>", usage = "the key(s) to add")
+		@Argument(metaVar = "<STDIN>", usage = "the key to add")
 		private List<String> addKeys = new ArrayList<String>();
 
 		@Option(name = "--permission", aliases = { "-p" }, metaVar = "PERMISSION", usage = "set the key access permission")
@@ -76,7 +76,7 @@
 		}
 
 		@Override
-		public void run() throws IOException, UnloggedFailure {
+		public void run() throws IOException, Failure {
 			String username = getContext().getClient().getUsername();
 			List<String> keys = readKeys(addKeys);
 			for (String key : keys) {
@@ -87,7 +87,7 @@
 						try {
 							sshKey.setPermission(ap);
 						} catch (IllegalArgumentException e) {
-							throw new UnloggedFailure(1, e.getMessage());
+							throw new Failure(1, e.getMessage());
 						}
 					}
 				}
@@ -105,22 +105,21 @@
 
 		private final String ALL = "ALL";
 
-		@Argument(metaVar = "<INDEX>|<KEY>|ALL", usage = "the key to remove", required = true)
-		private List<String> removeKeys = new ArrayList<String>();
+		@Argument(metaVar = "<INDEX>|ALL", usage = "the key to remove", required = true)
+		private List<String> keyParameters = new ArrayList<String>();
 
 		@Override
-		public void run() throws IOException, UnloggedFailure {
+		public void run() throws IOException, Failure {
 			String username = getContext().getClient().getUsername();
 			// remove a key that has been piped to the command
 			// or remove all keys
 
-			List<SshKey> currentKeys = getKeyManager().getKeys(username);
-			if (currentKeys == null || currentKeys.isEmpty()) {
+			List<SshKey> registeredKeys = new ArrayList<SshKey>(getKeyManager().getKeys(username));
+			if (registeredKeys.isEmpty()) {
 				throw new UnloggedFailure(1, "There are no registered keys!");
 			}
 
-			List<String> keys = readKeys(removeKeys);
-			if (keys.contains(ALL)) {
+			if (keyParameters.contains(ALL)) {
 				if (getKeyManager().removeAllKeys(username)) {
 					stdout.println("Removed all keys.");
 					log.info("removed all SSH public keys from {}", username);
@@ -128,32 +127,25 @@
 					log.warn("failed to remove all SSH public keys from {}", username);
 				}
 			} else {
-				for (String key : keys) {
+				for (String keyParameter : keyParameters) {
 					try {
 						// remove a key by it's index (1-based indexing)
-						int index = Integer.parseInt(key);
-						if (index > keys.size()) {
-							if (keys.size() == 1) {
-								throw new UnloggedFailure(1, "Invalid index specified. There is only 1 registered key.");
+						int index = Integer.parseInt(keyParameter);
+						if (index > registeredKeys.size()) {
+							if (keyParameters.size() == 1) {
+								throw new Failure(1, "Invalid index specified. There is only 1 registered key.");
 							}
-							throw new UnloggedFailure(1, String.format("Invalid index specified. There are %d registered keys.", keys.size()));
+							throw new Failure(1, String.format("Invalid index specified. There are %d registered keys.", registeredKeys.size()));
 						}
-						SshKey sshKey = currentKeys.get(index - 1);
+						SshKey sshKey = registeredKeys.get(index - 1);
 						if (getKeyManager().removeKey(username, sshKey)) {
 							stdout.println(String.format("Removed %s", sshKey.getFingerprint()));
 						} else {
-							throw new UnloggedFailure(1,  String.format("failed to remove #%s: %s", key, sshKey.getFingerprint()));
+							throw new Failure(1,  String.format("failed to remove #%s: %s", keyParameter, sshKey.getFingerprint()));
 						}
-					} catch (Exception e) {
-						// remove key by raw key data
-						SshKey sshKey = parseKey(key);
-						if (getKeyManager().removeKey(username, sshKey)) {
-							stdout.println(String.format("Removed %s", sshKey.getFingerprint()));
-							log.info("removed SSH public key {} from {}", sshKey.getFingerprint(), username);
-						} else {
-							log.warn("failed to remove SSH public key {} from {}", sshKey.getFingerprint(), username);
-							throw new UnloggedFailure(1,  String.format("failed to remove %s", sshKey.getFingerprint()));
-						}
+					} catch (NumberFormatException e) {
+						log.warn("failed to remove SSH public key {} from {}", keyParameter, username);
+						throw new Failure(1,  String.format("failed to remove key %s", keyParameter));
 					}
 				}
 			}
@@ -254,7 +246,7 @@
 		private List<String> values = new ArrayList<String>();
 
 		@Override
-		public void run() throws UnloggedFailure {
+		public void run() throws Failure {
 			final String username = getContext().getClient().getUsername();
 			IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager();
 			List<SshKey> keys = keyManager.getKeys(username);
@@ -268,7 +260,7 @@
 			if (keyManager.addKey(username, key)) {
 				stdout.println(String.format("Updated the comment for key #%d.", index));
 			} else {
-				throw new UnloggedFailure(1, String.format("Failed to update the comment for key #%d!", index));
+				throw new Failure(1, String.format("Failed to update the comment for key #%d!", index));
 			}
 		}
 
diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties
index 115dcd0..43d31d8 100644
--- a/src/main/java/log4j.properties
+++ b/src/main/java/log4j.properties
@@ -25,7 +25,9 @@
 #log4j.logger.net=INFO
 
 #log4j.logger.com.gitblit=DEBUG
-log4j.logger.org.apache.sshd=ERROR
+log4j.logger.com.gitblit.transport.ssh.SshServerSession=WARN
+log4j.logger.org.apache.sshd=WARN
+log4j.logger.org.apache.mina=WARN
 
 log4j.logger.org.apache.wicket=INFO
 log4j.logger.org.apache.wicket.RequestListenerInterface=WARN
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index b8d3b18..5a7dcea 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -64,7 +64,8 @@
 		SshDaemonTest.class, GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class,
 		FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class,
 		ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,
-		BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class })
+		BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class,
+		SshKeysDispatcherTest.class })
 public class GitBlitSuite {
 
 	public static final File BASEFOLDER = new File("data");
@@ -137,11 +138,16 @@
 		Executors.newSingleThreadExecutor().execute(new Runnable() {
 			@Override
 			public void run() {
-				GitBlitServer.main("--httpPort", "" + port, "--httpsPort", "0", "--shutdownPort",
-						"" + shutdownPort, "--gitPort", "" + gitPort, "--repositoriesFolder",
-						"\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService",
-						GitBlitSuite.USERSCONF.getAbsolutePath(), "--settings", GitBlitSuite.SETTINGS.getAbsolutePath(),
-						"--baseFolder", "data", "--sshPort", "" + sshPort);
+				GitBlitServer.main(
+						"--httpPort", "" + port,
+						"--httpsPort", "0",
+						"--shutdownPort", "" + shutdownPort,
+						"--gitPort", "" + gitPort,
+						"--sshPort", "" + sshPort,
+						"--repositoriesFolder", GitBlitSuite.REPOSITORIES.getAbsolutePath(),
+						"--userService", GitBlitSuite.USERSCONF.getAbsolutePath(),
+						"--settings", GitBlitSuite.SETTINGS.getAbsolutePath(),
+						"--baseFolder", "data");
 			}
 		});
 
diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java
index 620190e..dcaeaff 100644
--- a/src/test/java/com/gitblit/tests/SshDaemonTest.java
+++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java
@@ -15,102 +15,76 @@
  */
 package com.gitblit.tests;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.security.KeyPair;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.List;
 
-import org.apache.sshd.ClientChannel;
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.SshClient;
-import org.apache.sshd.common.KeyPairProvider;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.eclipse.jgit.util.FileUtils;
 import org.junit.Test;
 
 import com.gitblit.Constants;
-import com.gitblit.transport.ssh.IPublicKeyManager;
-import com.gitblit.transport.ssh.MemoryKeyManager;
-import com.gitblit.transport.ssh.SshKey;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.JGitUtils;
 
-public class SshDaemonTest extends GitblitUnitTest {
+public class SshDaemonTest extends SshUnitTest {
 
-	private static final AtomicBoolean started = new AtomicBoolean(false);
-	private static KeyPair pair;
+	static File ticgitFolder = new File(GitBlitSuite.REPOSITORIES, "working/ticgit");
 
-	@BeforeClass
-	public static void startGitblit() throws Exception {
-		started.set(GitBlitSuite.startGitblit());
-		pair = SshUtils.createTestHostKeyProvider().loadKey(KeyPairProvider.SSH_RSA);
-	}
-
-	@AfterClass
-	public static void stopGitblit() throws Exception {
-		if (started.get()) {
-			GitBlitSuite.stopGitblit();
-		}
-	}
-
-	protected MemoryKeyManager getKeyManager() {
-		IPublicKeyManager mgr = gitblit().getPublicKeyManager();
-		if (mgr instanceof MemoryKeyManager) {
-			return (MemoryKeyManager) gitblit().getPublicKeyManager();
-		} else {
-			throw new RuntimeException("unexpected key manager type " + mgr.getClass().getName());
-		}
-	}
-
-	@Before
-	public void prepare() {
-		MemoryKeyManager keyMgr = getKeyManager();
-		keyMgr.addKey("admin", new SshKey(pair.getPublic()));
-	}
-
-	@After
-	public void tearDown() {
-		MemoryKeyManager keyMgr = getKeyManager();
-		keyMgr.removeAllKeys("admin");
-	}
+	String url = GitBlitSuite.sshDaemonUrl;
 
 	@Test
 	public void testPublicKeyAuthentication() throws Exception {
-		SshClient client = SshClient.setUpDefaultClient();
-        client.start();
-        ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession();
-        pair.getPublic().getEncoded();
-        assertTrue(session.authPublicKey("admin", pair).await().isSuccess());
+		SshClient client = getClient();
+		ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession();
+		session.addPublicKeyIdentity(rwKeyPair);
+		assertTrue(session.auth().await().isSuccess());
 	}
 
 	@Test
 	public void testVersionCommand() throws Exception {
-		SshClient client = SshClient.setUpDefaultClient();
-        client.start();
-        ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession();
-        pair.getPublic().getEncoded();
-        assertTrue(session.authPublicKey("admin", pair).await().isSuccess());
+		String result = testSshCommand("version");
+		assertEquals(Constants.getGitBlitVersion(), result);
+	}
 
-        ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, "version");
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        Writer w = new OutputStreamWriter(baos);
-        w.close();
-        channel.setIn(new ByteArrayInputStream(baos.toByteArray()));
+	@Test
+	public void testCloneCommand() throws Exception {
+		if (ticgitFolder.exists()) {
+			GitBlitSuite.close(ticgitFolder);
+			FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE);
+		}
 
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ByteArrayOutputStream err = new ByteArrayOutputStream();
-        channel.setOut(out);
-        channel.setErr(err);
-        channel.open();
+		// set clone restriction
+		RepositoryModel model = repositories().getRepositoryModel("ticgit.git");
+		model.accessRestriction = AccessRestrictionType.CLONE;
+		model.authorizationControl = AuthorizationControl.NAMED;
+		repositories().updateRepositoryModel(model.name, model, false);
 
-        channel.waitFor(ClientChannel.CLOSED, 0);
+		JschConfigTestSessionFactory sessionFactory = new JschConfigTestSessionFactory(roKeyPair);
+		SshSessionFactory.setInstance(sessionFactory);
 
-        String result = out.toString().trim();
-        channel.close(false);
-        client.stop();
+		CloneCommand clone = Git.cloneRepository();
+		clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password));
+		clone.setURI(MessageFormat.format("{0}/ticgit.git", url));
+		clone.setDirectory(ticgitFolder);
+		clone.setBare(false);
+		clone.setCloneAllBranches(true);
+		Git git = clone.call();
+		List<RevCommit> commits = JGitUtils.getRevLog(git.getRepository(), 10);
+		GitBlitSuite.close(git);
+		assertEquals(10, commits.size());
 
-        assertEquals(Constants.getGitBlitVersion(), result);
-     }
+		// restore anonymous repository access
+		model.accessRestriction = AccessRestrictionType.NONE;
+		model.authorizationControl = AuthorizationControl.NAMED;
+		repositories().updateRepositoryModel(model.name, model, false);
+	}
 }
diff --git a/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java
new file mode 100644
index 0000000..8ccdc5b
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tests;
+
+import java.security.KeyPair;
+import java.util.List;
+
+import org.junit.Test;
+import org.parboiled.common.StringUtils;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.transport.ssh.SshKey;
+
+/**
+ * Tests the Keys Dispatcher and it's commands.
+ *
+ * @author James Moger
+ *
+ */
+public class SshKeysDispatcherTest extends SshUnitTest {
+
+	@Test
+	public void testKeysListCommand() throws Exception {
+		String result = testSshCommand("keys ls -L");
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 2, keys.size());
+		assertEquals(keys.get(0).getRawData() + "\n" + keys.get(1).getRawData(), result);
+	}
+
+	@Test
+	public void testKeysWhichCommand() throws Exception {
+		String result = testSshCommand("keys which -L");
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 2, keys.size());
+		assertEquals(keys.get(0).getRawData(), result);
+	}
+
+	@Test
+	public void testKeysRmCommand() throws Exception {
+		testSshCommand("keys rm 2");
+		String result = testSshCommand("keys ls -L");
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 1, keys.size());
+		assertEquals(keys.get(0).getRawData(), result);
+	}
+
+	@Test
+	public void testKeysRmAllByIndexCommand() throws Exception {
+		testSshCommand("keys rm 1 2");
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size());
+		try {
+			testSshCommand("keys ls -L");
+			assertTrue("Authentication worked without a public key?!", false);
+		} catch (AssertionError e) {
+			assertTrue(true);
+		}
+	}
+
+	@Test
+	public void testKeysRmAllCommand() throws Exception {
+		testSshCommand("keys rm ALL");
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size());
+		try {
+			testSshCommand("keys ls -L");
+			assertTrue("Authentication worked without a public key?!", false);
+		} catch (AssertionError e) {
+			assertTrue(true);
+		}
+	}
+
+	@Test
+	public void testKeysAddCommand() throws Exception {
+		KeyPair kp = generator.generateKeyPair();
+		SshKey key = new SshKey(kp.getPublic());
+		testSshCommand("keys add --permission R", key.getRawData());
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertEquals(String.format("There are %d keys!", keys.size()), 3, keys.size());
+		assertEquals(AccessPermission.CLONE, keys.get(2).getPermission());
+
+		String result = testSshCommand("keys ls -L");
+		StringBuilder sb = new StringBuilder();
+		for (SshKey sk : keys) {
+			sb.append(sk.getRawData());
+			sb.append('\n');
+		}
+		sb.setLength(sb.length() - 1);
+		assertEquals(sb.toString(), result);
+	}
+
+	@Test
+	public void testKeysCommentCommand() throws Exception {
+		List<SshKey> keys = getKeyManager().getKeys(username);
+		assertTrue(StringUtils.isEmpty(keys.get(0).getComment()));
+		String comment = "this is my comment";
+		testSshCommand(String.format("keys comment 1 %s", comment));
+
+		keys = getKeyManager().getKeys(username);
+		assertEquals(comment, keys.get(0).getComment());
+	}
+}
diff --git a/src/test/java/com/gitblit/tests/SshUnitTest.java b/src/test/java/com/gitblit/tests/SshUnitTest.java
new file mode 100644
index 0000000..43b51b7
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/SshUnitTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.tests;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.SocketAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.ClientChannel;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.SshClient;
+import org.apache.sshd.client.ServerKeyVerifier;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.transport.ssh.IPublicKeyManager;
+import com.gitblit.transport.ssh.MemoryKeyManager;
+import com.gitblit.transport.ssh.SshKey;
+
+/**
+ * Base class for SSH unit tests.
+ */
+public abstract class SshUnitTest extends GitblitUnitTest {
+
+	protected static final AtomicBoolean started = new AtomicBoolean(false);
+	protected static KeyPairGenerator generator;
+	protected KeyPair rwKeyPair;
+	protected KeyPair roKeyPair;
+	protected String username = "admin";
+	protected String password = "admin";
+
+	@BeforeClass
+	public static void startGitblit() throws Exception {
+		generator = SecurityUtils.getKeyPairGenerator("RSA");
+		started.set(GitBlitSuite.startGitblit());
+	}
+
+	@AfterClass
+	public static void stopGitblit() throws Exception {
+		if (started.get()) {
+			GitBlitSuite.stopGitblit();
+		}
+	}
+
+	protected MemoryKeyManager getKeyManager() {
+		IPublicKeyManager mgr = gitblit().getPublicKeyManager();
+		if (mgr instanceof MemoryKeyManager) {
+			return (MemoryKeyManager) gitblit().getPublicKeyManager();
+		} else {
+			throw new RuntimeException("unexpected key manager type " + mgr.getClass().getName());
+		}
+	}
+
+	@Before
+	public void prepare() {
+		rwKeyPair = generator.generateKeyPair();
+
+		MemoryKeyManager keyMgr = getKeyManager();
+		keyMgr.addKey(username, new SshKey(rwKeyPair.getPublic()));
+
+		roKeyPair = generator.generateKeyPair();
+		SshKey sshKey = new SshKey(roKeyPair.getPublic());
+		sshKey.setPermission(AccessPermission.CLONE);
+		keyMgr.addKey(username, sshKey);
+	}
+
+	@After
+	public void tearDown() {
+		MemoryKeyManager keyMgr = getKeyManager();
+		keyMgr.removeAllKeys(username);
+	}
+
+	protected SshClient getClient() {
+		SshClient client = SshClient.setUpDefaultClient();
+		client.setServerKeyVerifier(new ServerKeyVerifier() {
+			@Override
+			public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+				return true;
+			}
+		});
+		client.start();
+		return client;
+	}
+
+	protected String testSshCommand(String cmd) throws IOException, InterruptedException {
+		return testSshCommand(cmd, null);
+	}
+
+	protected String testSshCommand(String cmd, String stdin) throws IOException, InterruptedException {
+		SshClient client = getClient();
+		ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession();
+		session.addPublicKeyIdentity(rwKeyPair);
+		assertTrue(session.auth().await().isSuccess());
+
+		ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, cmd);
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		if (stdin != null) {
+			Writer w = new OutputStreamWriter(baos);
+			w.write(stdin);
+			w.close();
+		}
+		channel.setIn(new ByteArrayInputStream(baos.toByteArray()));
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ByteArrayOutputStream err = new ByteArrayOutputStream();
+		channel.setOut(out);
+		channel.setErr(err);
+		channel.open();
+
+		channel.waitFor(ClientChannel.CLOSED, 0);
+
+		String result = out.toString().trim();
+		channel.close(false);
+		client.stop();
+		return result;
+	}
+}
diff --git a/src/test/java/com/gitblit/tests/SshUtils.java b/src/test/java/com/gitblit/tests/SshUtils.java
deleted file mode 100644
index 9760f75..0000000
--- a/src/test/java/com/gitblit/tests/SshUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.tests;
-
-import java.io.File;
-import java.net.ServerSocket;
-import java.net.URISyntaxException;
-import java.net.URL;
-
-import org.apache.sshd.common.KeyPairProvider;
-import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
-
-public class SshUtils {
-
-    public static KeyPairProvider createTestHostKeyProvider() {
-        return new SimpleGeneratorHostKeyProvider("target/hostkey.rsa", "RSA");
-    }
-
-    public static FileKeyPairProvider createTestKeyPairProvider(String resource) {
-        return new FileKeyPairProvider(new String[] { getFile(resource) });
-    }
-
-    public static int getFreePort() throws Exception {
-        ServerSocket s = new ServerSocket(0);
-        try {
-            return s.getLocalPort();
-        } finally {
-            s.close();
-        }
-    }
-
-    private static String getFile(String resource) {
-        URL url = SshUtils.class.getClassLoader().getResource(resource);
-        File f;
-        try {
-            f = new File(url.toURI());
-        } catch(URISyntaxException e) {
-            f = new File(url.getPath());
-        }
-        return f.toString();
-    }
-
-    public static void deleteRecursive(File file) {
-        if (file != null) {
-            if (file.isDirectory()) {
-                File[] children = file.listFiles();
-                if (children != null) {
-                    for (File child : children) {
-                        deleteRecursive(child);
-                    }
-                }
-            }
-            file.delete();
-        }
-    }
-
-}

--
Gitblit v1.9.1