From 20714aee0d2d2a989d93d6065e081aed8ac85fbf Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 10 Oct 2012 00:05:34 -0400
Subject: [PATCH] Finer-grained repository access permissions (issue 36)

---
 tests/com/gitblit/tests/GitServletTest.java |  217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 217 insertions(+), 0 deletions(-)

diff --git a/tests/com/gitblit/tests/GitServletTest.java b/tests/com/gitblit/tests/GitServletTest.java
index bdbb2a5..09e0e5a 100644
--- a/tests/com/gitblit/tests/GitServletTest.java
+++ b/tests/com/gitblit/tests/GitServletTest.java
@@ -13,18 +13,28 @@
 
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
 import org.eclipse.jgit.util.FileUtils;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.GitBlit;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.JGitUtils;
 
 public class GitServletTest {
 
@@ -233,6 +243,213 @@
 		}
 		close(git);
 	}
+
+	@Test
+	public void testBlockClone() throws Exception {
+		testRefChange(AccessPermission.VIEW, null, null, null);
+	}
+
+	@Test
+	public void testBlockPush() throws Exception {
+		testRefChange(AccessPermission.CLONE, null, null, null);
+	}
+
+	@Test
+	public void testBlockBranchCreation() throws Exception {
+		testRefChange(AccessPermission.PUSH, Status.REJECTED_OTHER_REASON, null, null);
+	}
+
+	@Test
+	public void testBlockBranchDeletion() throws Exception {
+		testRefChange(AccessPermission.CREATE, Status.OK, Status.REJECTED_OTHER_REASON, null);
+	}
+	
+	@Test
+	public void testBlockBranchRewind() throws Exception {
+		testRefChange(AccessPermission.DELETE, Status.OK, Status.OK, Status.REJECTED_OTHER_REASON);
+	}
+
+	@Test
+	public void testBranchRewind() throws Exception {		
+		testRefChange(AccessPermission.REWIND, Status.OK, Status.OK, Status.OK);
+	}
+
+	private void testRefChange(AccessPermission permission, Status expectedCreate, Status expectedDelete, Status expectedRewind) throws Exception {
+
+		UserModel user = new UserModel("james");
+		user.password = "james";
+		
+		if (GitBlit.self().getUserModel(user.username) != null) {
+			GitBlit.self().deleteUser(user.username);
+		}
+		
+		CredentialsProvider cp = new UsernamePasswordCredentialsProvider(user.username, user.password);
+		
+		// fork from original to a temporary bare repo
+		File refChecks = new File(GitBlitSuite.REPOSITORIES, "refchecks/ticgit.git");
+		if (refChecks.exists()) {
+			FileUtils.delete(refChecks, FileUtils.RECURSIVE);
+		}
+		CloneCommand clone = Git.cloneRepository();
+		clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url));
+		clone.setDirectory(refChecks);
+		clone.setBare(true);
+		clone.setCloneAllBranches(true);
+		clone.setCredentialsProvider(cp);
+		close(clone.call());
+
+		// elevate repository to clone permission
+		RepositoryModel model = GitBlit.self().getRepositoryModel("refchecks/ticgit.git");
+		switch (permission) {
+			case VIEW:
+				model.accessRestriction = AccessRestrictionType.CLONE;
+				break;
+			case CLONE:
+				model.accessRestriction = AccessRestrictionType.CLONE;
+				break;
+			default:
+				model.accessRestriction = AccessRestrictionType.PUSH;
+		}
+		model.authorizationControl = AuthorizationControl.NAMED;
+		
+		// grant user specified
+		user.setRepositoryPermission(model.name, permission);
+
+		GitBlit.self().updateUserModel(user.username, user, true);
+		GitBlit.self().updateRepositoryModel(model.name, model, false);
+
+		// clone temp bare repo to working copy
+		File local = new File(GitBlitSuite.REPOSITORIES, "refchecks/ticgit-wc");
+		if (local.exists()) {
+			FileUtils.delete(local, FileUtils.RECURSIVE);
+		}
+		clone = Git.cloneRepository();
+		clone.setURI(MessageFormat.format("{0}/git/{1}", url, model.name));
+		clone.setDirectory(local);
+		clone.setBare(false);
+		clone.setCloneAllBranches(true);
+		clone.setCredentialsProvider(cp);
+		
+		try {
+			close(clone.call());
+		} catch (GitAPIException e) {
+			if (permission.atLeast(AccessPermission.CLONE)) {
+				throw e;
+			} else {
+				// user does not have clone permission
+				assertTrue(e.getMessage(), e.getMessage().contains("not permitted"));				
+				return;
+			}
+		}
+		
+		Git git = Git.open(local);
+		
+		// commit a file and push it
+		File file = new File(local, "PUSHCHK");
+		OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
+		BufferedWriter w = new BufferedWriter(os);
+		w.write("// " + new Date().toString() + "\n");
+		w.close();
+		git.add().addFilepattern(file.getName()).call();
+		git.commit().setMessage("push test").call();
+		Iterable<PushResult> results = null;
+		try {
+			results = git.push().setCredentialsProvider(cp).setRemote("origin").call();
+		} catch (GitAPIException e) {
+			if (permission.atLeast(AccessPermission.PUSH)) {
+				throw e;
+			} else {
+				// user does not have push permission
+				assertTrue(e.getMessage(), e.getMessage().contains("not permitted"));
+				close(git);
+				return;
+			}
+		}
+		
+		for (PushResult result : results) {
+			RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");
+			Status status = ref.getStatus();
+			if (permission.atLeast(AccessPermission.PUSH)) {
+				assertTrue("User failed to push commit?! " + status.name(), Status.OK.equals(status));
+			} else {
+				assertTrue("User was able to push commit! " + status.name(), Status.REJECTED_OTHER_REASON.equals(status));
+				close(git);
+				// skip delete test
+				return;
+			}
+		}
+		
+		// create a local branch and push the new branch back to the origin				
+		git.branchCreate().setName("protectme").call();
+		RefSpec refSpec = new RefSpec("refs/heads/protectme:refs/heads/protectme");
+		results = git.push().setCredentialsProvider(cp).setRefSpecs(refSpec).setRemote("origin").call();
+		for (PushResult result : results) {
+			RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/protectme");
+			Status status = ref.getStatus();
+			if (Status.OK.equals(expectedCreate)) {
+				assertTrue("User failed to push creation?! " + status.name(), status.equals(expectedCreate));
+			} else {
+				assertTrue("User was able to push ref creation! " + status.name(), status.equals(expectedCreate));
+				close(git);
+				// skip delete test
+				return;
+			}
+		}
+		
+		// delete the branch locally
+		git.branchDelete().setBranchNames("protectme").call();
+		
+		// push a delete ref command
+		refSpec = new RefSpec(":refs/heads/protectme");
+		results = git.push().setCredentialsProvider(cp).setRefSpecs(refSpec).setRemote("origin").call();
+		for (PushResult result : results) {
+			RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/protectme");
+			Status status = ref.getStatus();
+			if (Status.OK.equals(expectedDelete)) {
+				assertTrue("User failed to push ref deletion?! " + status.name(), status.equals(Status.OK));
+			} else {
+				assertTrue("User was able to push ref deletion?! " + status.name(), status.equals(expectedDelete));
+				close(git);
+				// skip rewind test
+				return;
+			}
+		}
+		
+		// rewind master by two commits
+		git.reset().setRef("HEAD~2").setMode(ResetType.HARD).call();
+		
+		// commit a change on this detached HEAD
+		file = new File(local, "REWINDCHK");
+		os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
+		w = new BufferedWriter(os);
+		w.write("// " + new Date().toString() + "\n");
+		w.close();
+		git.add().addFilepattern(file.getName()).call();
+		RevCommit commit = git.commit().setMessage("rewind master and new commit").call();
+		
+		// Reset master to our new commit now we our local branch tip is no longer
+		// upstream of the remote branch tip.  It is an alternate tip of the branch.
+		JGitUtils.setBranchRef(git.getRepository(), "refs/heads/master", commit.getName());
+		
+		// Try pushing our new tip to the origin.
+		// This requires the server to "rewind" it's master branch and update it
+		// to point to our alternate tip.  This leaves the original master tip
+		// unreferenced.
+		results = git.push().setCredentialsProvider(cp).setRemote("origin").setForce(true).call();
+		for (PushResult result : results) {
+			RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");
+			Status status = ref.getStatus();
+			if (Status.OK.equals(expectedRewind)) {
+				assertTrue("User failed to rewind master?! " + status.name(), status.equals(expectedRewind));
+			} else {
+				assertTrue("User was able to rewind master?! " + status.name(), status.equals(expectedRewind));
+			}
+		}
+		close(git);
+		
+		GitBlit.self().deleteUser(user.username);
+	}
+
 	
 	private void close(Git git) {
 		// really close the repository

--
Gitblit v1.9.1