From c78b25d102fe700617011a4c8acc0d35f9a9e6ca Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 19:00:52 -0400
Subject: [PATCH] Support specifying permission levels for SSH public keys

---
 src/main/java/com/gitblit/transport/ssh/SshKey.java              |   47 +++++++++++++++
 src/main/java/com/gitblit/transport/ssh/FileKeyManager.java      |   48 ++++++++++-----
 src/main/java/com/gitblit/transport/ssh/git/Upload.java          |    5 +
 src/main/java/com/gitblit/transport/ssh/git/Receive.java         |    5 +
 src/main/java/com/gitblit/Constants.java                         |    2 
 src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java |   34 +++++++++-
 6 files changed, 120 insertions(+), 21 deletions(-)

diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 56dfec0..26e0de3 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -423,6 +423,8 @@
 
 		public static final AccessPermission [] NEWPERMISSIONS = { EXCLUDE, VIEW, CLONE, PUSH, CREATE, DELETE, REWIND };
 
+		public static final AccessPermission [] SSHPERMISSIONS = { VIEW, CLONE, PUSH };
+
 		public static AccessPermission LEGACY = REWIND;
 
 		public final String code;
diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
index 77f818c..ae4588a 100644
--- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
@@ -23,6 +23,7 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Keys;
 import com.gitblit.manager.IRuntimeManager;
 import com.google.common.base.Charsets;
@@ -105,8 +106,18 @@
 						// skip comments
 						continue;
 					}
-					SshKey key = new SshKey(entry);
-					list.add(key);
+					String [] parts = entry.split(" ", 2);
+					AccessPermission perm = AccessPermission.fromCode(parts[0]);
+					if (perm.equals(AccessPermission.NONE)) {
+						// ssh-rsa DATA COMMENT
+						SshKey key = new SshKey(entry);
+						list.add(key);
+					} else if (perm.exceeds(AccessPermission.NONE)) {
+						// PERMISSION ssh-rsa DATA COMMENT
+						SshKey key = new SshKey(parts[1]);
+						key.setPermission(perm);
+						list.add(key);
+					}
 				}
 
 				if (list.isEmpty()) {
@@ -129,7 +140,6 @@
 	@Override
 	public boolean addKey(String username, SshKey key) {
 		try {
-			String newKey = stripCommentFromKey(key.getRawData());
 			boolean replaced = false;
 			List<String> lines = new ArrayList<String>();
 			File keystore = getKeystore(username);
@@ -147,10 +157,10 @@
 						continue;
 					}
 
-					String oldKey = stripCommentFromKey(line);
-					if (newKey.equals(oldKey)) {
+					SshKey oldKey = parseKey(line);
+					if (key.equals(oldKey)) {
 						// replace key
-						lines.add(key.getRawData());
+						lines.add(key.getPermission() + " " + key.getRawData());
 						replaced = true;
 					} else {
 						// retain key
@@ -161,7 +171,7 @@
 
 			if (!replaced) {
 				// new key, append
-				lines.add(key.getRawData());
+				lines.add(key.getPermission() + " " + key.getRawData());
 			}
 
 			// write keystore
@@ -182,8 +192,6 @@
 	@Override
 	public boolean removeKey(String username, SshKey key) {
 		try {
-			String rmKey = stripCommentFromKey(key.getRawData());
-
 			File keystore = getKeystore(username);
 			if (keystore.exists()) {
 				List<String> lines = new ArrayList<String>();
@@ -201,8 +209,8 @@
 					}
 
 					// only include keys that are NOT rmKey
-					String oldKey = stripCommentFromKey(line);
-					if (!rmKey.equals(oldKey)) {
+					SshKey oldKey = parseKey(line);
+					if (!key.equals(oldKey)) {
 						lines.add(entry);
 					}
 				}
@@ -242,10 +250,18 @@
 		return keys;
 	}
 
-	/* Strips the comment from the key data and eliminates whitespace diffs */
-	protected String stripCommentFromKey(String data) {
-		String [] cols = data.split(" ", 3);
-		String key = Joiner.on(" ").join(cols[0], cols[1]);
-		return key;
+	protected SshKey parseKey(String line) {
+		String [] parts = line.split(" ", 2);
+		AccessPermission perm = AccessPermission.fromCode(parts[0]);
+		if (perm.equals(AccessPermission.NONE)) {
+			// ssh-rsa DATA COMMENT
+			SshKey key = new SshKey(line);
+			return key;
+		} else {
+			// PERMISSION ssh-rsa DATA COMMENT
+			SshKey key = new SshKey(parts[1]);
+			key.setPermission(perm);
+			return key;
+		}
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKey.java b/src/main/java/com/gitblit/transport/ssh/SshKey.java
index cb5ee09..6ac0cdc 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshKey.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshKey.java
@@ -2,12 +2,15 @@
 
 import java.io.Serializable;
 import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.List;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.util.Buffer;
 import org.eclipse.jgit.lib.Constants;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.utils.StringUtils;
 
 /**
@@ -30,13 +33,17 @@
 
 	private String toString;
 
+	private AccessPermission permission;
+
 	public SshKey(String data) {
 		this.rawData = data;
+		this.permission = AccessPermission.PUSH;
 	}
 
 	public SshKey(PublicKey key) {
 		this.publicKey = key;
 		this.comment = "";
+		this.permission = AccessPermission.PUSH;
 	}
 
 	public PublicKey getPublicKey() {
@@ -78,6 +85,46 @@
 		}
 	}
 
+	/**
+	 * Returns true if this key may be used to clone or fetch.
+	 *
+	 * @return true if this key can be used to clone or fetch
+	 */
+	public boolean canClone() {
+		return permission.atLeast(AccessPermission.CLONE);
+	}
+
+	/**
+	 * Returns true if this key may be used to push changes.
+	 *
+	 * @return true if this key can be used to push changes
+	 */
+	public boolean canPush() {
+		return permission.atLeast(AccessPermission.PUSH);
+	}
+
+	/**
+	 * Returns the access permission for the key.
+	 *
+	 * @return the access permission for the key
+	 */
+	public AccessPermission getPermission() {
+		return permission;
+	}
+
+	/**
+	 * Control the access permission assigned to this key.
+	 *
+	 * @param value
+	 */
+	public void setPermission(AccessPermission value) throws IllegalArgumentException {
+		List<AccessPermission> permitted = Arrays.asList(AccessPermission.SSHPERMISSIONS);
+		if (!permitted.contains(value)) {
+			throw new IllegalArgumentException("Illegal SSH public key permission specified: " + value);
+		}
+		this.permission = value;
+	}
+
 	public String getRawData() {
 		if (rawData == null && publicKey != null) {
 			// build the raw data manually from the public key
diff --git a/src/main/java/com/gitblit/transport/ssh/git/Receive.java b/src/main/java/com/gitblit/transport/ssh/git/Receive.java
index 94d0998..f0d86f0 100644
--- a/src/main/java/com/gitblit/transport/ssh/git/Receive.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.java
@@ -17,12 +17,17 @@
 
 import org.eclipse.jgit.transport.ReceivePack;
 
+import com.gitblit.transport.ssh.SshKey;
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 
 @CommandMetaData(name = "git-receive-pack", description = "Receives pushes from a client", hidden = true)
 public class Receive extends BaseGitCommand {
 	@Override
 	protected void runImpl() throws Failure {
+		SshKey key = getContext().getClient().getKey();
+		if (key != null && !key.canPush()) {
+			throw new Failure(1, "Sorry, your SSH public key is not allowed to push changes!");
+		}
 		try {
 			ReceivePack rp = receivePackFactory.create(getContext().getClient(), repo);
 			rp.receive(in, out, null);
diff --git a/src/main/java/com/gitblit/transport/ssh/git/Upload.java b/src/main/java/com/gitblit/transport/ssh/git/Upload.java
index c4dfa80..11a33ce 100644
--- a/src/main/java/com/gitblit/transport/ssh/git/Upload.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.java
@@ -17,6 +17,7 @@
 
 import org.eclipse.jgit.transport.UploadPack;
 
+import com.gitblit.transport.ssh.SshKey;
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 
 @CommandMetaData(name = "git-upload-pack", description = "Sends packs to a client for clone and fetch", hidden = true)
@@ -24,6 +25,10 @@
 	@Override
 	protected void runImpl() throws Failure {
 		try {
+			SshKey key = getContext().getClient().getKey();
+			if (key != null && !key.canClone()) {
+				throw new Failure(1, "Sorry, your SSH public key is not allowed to clone!");
+			}
 			UploadPack up = uploadPackFactory.create(getContext().getClient(), repo);
 			up.upload(in, out, null);
 		} catch (Exception e) {
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 ad37306..62daec6 100644
--- a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
@@ -24,6 +24,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.models.UserModel;
 import com.gitblit.transport.ssh.IPublicKeyManager;
 import com.gitblit.transport.ssh.SshKey;
@@ -33,6 +34,7 @@
 import com.gitblit.transport.ssh.commands.UsageExample;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
+import com.gitblit.utils.StringUtils;
 import com.google.common.base.Joiner;
 
 /**
@@ -54,7 +56,7 @@
 	}
 
 	@CommandMetaData(name = "add", description = "Add an SSH public key to your account")
-	@UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd} -", description = "Upload your SSH public key and add it to your account")
+	@UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd}", description = "Upload your SSH public key and add it to your account")
 	public static class AddKey extends BaseKeyCommand {
 
 		protected final Logger log = LoggerFactory.getLogger(getClass());
@@ -62,12 +64,33 @@
 		@Argument(metaVar = "<KEY>", usage = "the key(s) to add")
 		private List<String> addKeys = new ArrayList<String>();
 
+		@Option(name = "--permission", aliases = { "-p" }, metaVar = "PERMISSION", usage = "set the key access permission")
+		private String permission;
+
+		@Override
+		protected String getUsageText() {
+			String permissions = Joiner.on(", ").join(AccessPermission.SSHPERMISSIONS);
+			StringBuilder sb = new StringBuilder();
+			sb.append("Valid SSH public key permissions are:\n   ").append(permissions);
+			return sb.toString();
+		}
+
 		@Override
 		public void run() throws IOException, UnloggedFailure {
 			String username = getContext().getClient().getUsername();
 			List<String> keys = readKeys(addKeys);
 			for (String key : keys) {
 				SshKey sshKey = parseKey(key);
+				if (!StringUtils.isEmpty(permission)) {
+					AccessPermission ap = AccessPermission.fromCode(permission);
+					if (ap.exceeds(AccessPermission.NONE)) {
+						try {
+							sshKey.setPermission(ap);
+						} catch (IllegalArgumentException e) {
+							throw new UnloggedFailure(1, e.getMessage());
+						}
+					}
+				}
 				getKeyManager().addKey(username, sshKey);
 				log.info("added SSH public key for {}", username);
 			}
@@ -167,14 +190,15 @@
 		}
 
 		protected void asTable(List<SshKey> keys) {
-			String[] headers = { "#", "Fingerprint", "Comment", "Type" };
+			String[] headers = { "#", "Fingerprint", "Comment", "Permission", "Type" };
 			int len = keys == null ? 0 : keys.size();
 			Object[][] data = new Object[len][];
 			for (int i = 0; i < len; i++) {
 				// show 1-based index numbers with the fingerprint
 				// this is useful for comparing with "ssh-add -l"
 				SshKey k = keys.get(i);
-				data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() };
+				data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(),
+						k.getPermission(), k.getAlgorithm() };
 			}
 
 			stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
@@ -211,9 +235,9 @@
 		}
 
 		protected void asTable(int index, SshKey key) {
-			String[] headers = { "#", "Fingerprint", "Comment", "Type" };
+			String[] headers = { "#", "Fingerprint", "Comment", "Permission", "Type" };
 			Object[][] data = new Object[1][];
-			data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getAlgorithm() };
+			data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getPermission(), key.getAlgorithm() };
 
 			stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
 		}

--
Gitblit v1.9.1