From e9872c8ca4d9af41794a851f2f81ed21c65bb85b 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] Allow specifying accepted PUSH transports

---
 src/main/java/com/gitblit/git/GitblitReceivePackFactory.java |   46 +++++++++++++++
 src/main/java/com/gitblit/Constants.java                     |   19 ++++++
 src/main/java/com/gitblit/models/RepositoryUrl.java          |    3 +
 releases.moxie                                               |    3 +
 src/main/distrib/data/gitblit.properties                     |   10 +++
 src/main/java/com/gitblit/GitBlit.java                       |   76 +++++++++++++++++++++++++
 6 files changed, 157 insertions(+), 0 deletions(-)

diff --git a/releases.moxie b/releases.moxie
index 89a7a5f..32da215 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -23,10 +23,12 @@
     - Redirect to summary page on edit repository (issue-405)
     - Option to allow LDAP users to directly authenticate without performing LDAP searches (pr-162)
     - Replace JCommander with args4j to be consistent with other tools (ticket-28)
+    - Sort repository urls by descending permissions and by transport security within equal permissions
     additions:
     - Added an SSH daemon with public key authentication (issue-369, ticket-6)
     - Added beginnings of a plugin framework for extending Gitblit (issue-381, ticket-23)
     - Added a French translation (pr-163)
+    - Added a setting to control what transports may be used for pushes
     dependencyChanges:
     - args4j 2.0.26
     - JGit 3.3.1
@@ -41,6 +43,7 @@
     settings:
     - { name: 'realm.ldap.bindpattern', defaultValue: ' ' }
     - { name: 'tickets.closeOnPushCommitMessageRegex', defaultValue: '(?:fixes|closes)[\\s-]+#?(\\d+)' }
+    - { name: 'git.acceptedPushTransports', defaultValue: ' ' }
     - { name: 'git.sshPort', defaultValue: '29418' }
     - { name: 'git.sshBindInterface', defaultValue: ' ' }
     - { name: 'git.sshKeysManager', defaultValue: 'com.gitblit.transport.ssh.FileKeyManager' }
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index c52423b..c7e3a21 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -173,6 +173,16 @@
 # SINCE 0.9.0
 git.onlyAccessBareRepositories = false
 
+
+# Specify the list of acceptable transports for pushes.
+# If this setting is empty, all transports are acceptable.
+#
+# Valid choices are: GIT HTTP HTTPS SSH
+#
+# SINCE 1.5.0
+# SPACE-DELIMITED
+git.acceptedPushTransports = HTTP HTTPS SSH
+
 # Allow an authenticated user to create a destination repository on a push if
 # the repository does not already exist.
 #
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 26e0de3..af53399 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -540,6 +540,25 @@
 		}
 	}
 
+	public static enum Transport {
+		// ordered for url advertisements, assuming equal access permissions
+		SSH, HTTPS, HTTP, GIT;
+
+		public static Transport fromString(String value) {
+			for (Transport t : values()) {
+				if (t.name().equalsIgnoreCase(value)) {
+					return t;
+				}
+			}
+			return null;
+		}
+
+		public static Transport fromUrl(String url) {
+			String scheme = url.substring(0, url.indexOf("://"));
+			return fromString(scheme);
+		}
+	}
+
 	@Documented
 	@Retention(RetentionPolicy.RUNTIME)
 	public @interface Unused {
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 26ab3f3..0834252 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -17,12 +17,17 @@
 
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.inject.Singleton;
 import javax.servlet.http.HttpServletRequest;
 
 import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.Transport;
 import com.gitblit.manager.GitblitManager;
 import com.gitblit.manager.IAuthenticationManager;
 import com.gitblit.manager.IFederationManager;
@@ -116,6 +121,32 @@
 		return new Object [] { new GitBlitModule()};
 	}
 
+	protected boolean acceptPush(Transport byTransport) {
+		if (byTransport == null) {
+			logger.info("Unknown transport, push rejected!");
+			return false;
+		}
+
+		Set<Transport> transports = new HashSet<Transport>();
+		for (String value : getSettings().getStrings(Keys.git.acceptedPushTransports)) {
+			Transport transport = Transport.fromString(value);
+			if (transport == null) {
+				logger.info(String.format("Ignoring unknown registered transport %s", value));
+				continue;
+			}
+
+			transports.add(transport);
+		}
+
+		if (transports.isEmpty()) {
+			// no transports are explicitly specified, all are acceptable
+			return true;
+		}
+
+		// verify that the transport is permitted
+		return transports.contains(byTransport);
+	}
+
 	/**
 	 * Returns a list of repository URLs and the user access permission.
 	 *
@@ -137,6 +168,12 @@
 		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {
 			AccessPermission permission = user.getRepositoryPermission(repository).permission;
 			if (permission.exceeds(AccessPermission.NONE)) {
+				Transport transport = Transport.fromString(request.getScheme());
+				if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(transport)) {
+					// downgrade the repo permission for this transport
+					// because it is not an acceptable PUSH transport
+					permission = AccessPermission.CLONE;
+				}
 				list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission));
 			}
 		}
@@ -146,6 +183,12 @@
 		if (!StringUtils.isEmpty(sshDaemonUrl)) {
 			AccessPermission permission = user.getRepositoryPermission(repository).permission;
 			if (permission.exceeds(AccessPermission.NONE)) {
+				if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.SSH)) {
+					// downgrade the repo permission for this transport
+					// because it is not an acceptable PUSH transport
+					permission = AccessPermission.CLONE;
+				}
+
 				list.add(new RepositoryUrl(sshDaemonUrl, permission));
 			}
 		}
@@ -155,6 +198,11 @@
 		if (!StringUtils.isEmpty(gitDaemonUrl)) {
 			AccessPermission permission = servicesManager.getGitDaemonAccessPermission(user, repository);
 			if (permission.exceeds(AccessPermission.NONE)) {
+				if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.GIT)) {
+					// downgrade the repo permission for this transport
+					// because it is not an acceptable PUSH transport
+					permission = AccessPermission.CLONE;
+				}
 				list.add(new RepositoryUrl(gitDaemonUrl, permission));
 			}
 		}
@@ -173,6 +221,34 @@
 				list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null));
 			}
 		}
+
+		// sort transports by highest permission and then by transport security
+		Collections.sort(list, new Comparator<RepositoryUrl>() {
+
+			@Override
+			public int compare(RepositoryUrl o1, RepositoryUrl o2) {
+				if (!o1.isExternal() && o2.isExternal()) {
+					// prefer Gitblit over external
+					return -1;
+				} else if (o1.isExternal() && !o2.isExternal()) {
+					// prefer Gitblit over external
+					return 1;
+				} else if (o1.isExternal() && o2.isExternal()) {
+					// sort by Transport ordinal
+					return o1.transport.compareTo(o2.transport);
+				} else if (o1.permission.exceeds(o2.permission)) {
+					// prefer highest permission
+					return -1;
+				} else if (o2.permission.exceeds(o1.permission)) {
+					// prefer highest permission
+					return 1;
+				}
+
+				// prefer more secure transports
+				return o1.transport.compareTo(o2.transport);
+			}
+		});
+
 		return list;
 	}
 
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
index 41e348b..afda23b 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
@@ -15,6 +15,9 @@
  */
 package com.gitblit.git;
 
+import java.util.HashSet;
+import java.util.Set;
+
 import javax.servlet.http.HttpServletRequest;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -26,6 +29,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Constants.Transport;
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.manager.IGitblit;
@@ -66,6 +70,7 @@
 		String origin = "";
 		String gitblitUrl = "";
 		int timeout = 0;
+		Transport transport = null;
 
 		if (req instanceof HttpServletRequest) {
 			// http/https request may or may not be authenticated
@@ -82,6 +87,13 @@
 					user = u;
 				}
 			}
+
+			// determine the transport
+			if ("http".equals(client.getScheme())) {
+				transport = Transport.HTTP;
+			} else if ("https".equals(client.getScheme())) {
+				transport = Transport.HTTPS;
+			}
 		} else if (req instanceof GitDaemonClient) {
 			// git daemon request is always anonymous
 			GitDaemonClient client = (GitDaemonClient) req;
@@ -90,12 +102,20 @@
 
 			// set timeout from Git daemon
 			timeout = client.getDaemon().getTimeout();
+
+			transport = Transport.GIT;
 		} else if (req instanceof SshDaemonClient) {
 			// SSH request is always authenticated
 			SshDaemonClient client = (SshDaemonClient) req;
 			repositoryName = client.getRepositoryName();
 			origin = client.getRemoteAddress().toString();
 			user = client.getUser();
+
+			transport = Transport.SSH;
+		}
+
+		if (!acceptPush(transport)) {
+			throw new ServiceNotAuthorizedException();
 		}
 
 		boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false);
@@ -125,4 +145,30 @@
 
 		return rp;
 	}
+
+	protected boolean acceptPush(Transport byTransport) {
+		if (byTransport == null) {
+			logger.info("Unknown transport, push rejected!");
+			return false;
+		}
+
+		Set<Transport> transports = new HashSet<Transport>();
+		for (String value : gitblit.getSettings().getStrings(Keys.git.acceptedPushTransports)) {
+			Transport transport = Transport.fromString(value);
+			if (transport == null) {
+				logger.info(String.format("Ignoring unknown registered transport %s", value));
+				continue;
+			}
+
+			transports.add(transport);
+		}
+
+		if (transports.isEmpty()) {
+			// no transports are explicitly specified, all are acceptable
+			return true;
+		}
+
+		// verify that the transport is permitted
+		return transports.contains(byTransport);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RepositoryUrl.java b/src/main/java/com/gitblit/models/RepositoryUrl.java
index a24def5..d155dbd 100644
--- a/src/main/java/com/gitblit/models/RepositoryUrl.java
+++ b/src/main/java/com/gitblit/models/RepositoryUrl.java
@@ -18,6 +18,7 @@
 import java.io.Serializable;
 
 import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.Transport;
 
 /**
  * Represents a git repository url and it's associated access permission for the
@@ -30,10 +31,12 @@
 
 	private static final long serialVersionUID = 1L;
 
+	public final Transport transport;
 	public final String url;
 	public final AccessPermission permission;
 
 	public RepositoryUrl(String url, AccessPermission permission) {
+		this.transport = Transport.fromUrl(url);
 		this.url = url;
 		this.permission = permission;
 	}

--
Gitblit v1.9.1