]> source.dussan.org Git - gitblit.git/commitdiff
Experimental committer verification
authorJames Moger <james.moger@gitblit.com>
Thu, 11 Oct 2012 22:11:50 +0000 (18:11 -0400)
committerJames Moger <james.moger@gitblit.com>
Thu, 11 Oct 2012 22:11:50 +0000 (18:11 -0400)
docs/01_setup.mkd
docs/04_releases.mkd
src/com/gitblit/GitBlit.java
src/com/gitblit/GitServlet.java
src/com/gitblit/models/RepositoryModel.java
src/com/gitblit/models/UserModel.java
src/com/gitblit/wicket/GitBlitWebApp.properties
src/com/gitblit/wicket/pages/EditRepositoryPage.html
src/com/gitblit/wicket/pages/EditRepositoryPage.java
tests/com/gitblit/tests/GitBlitTest.java
tests/com/gitblit/tests/GitServletTest.java

index 3123aa1ff17561a9486a3db5bbfe7ba9bb5cd4a7..a9ce767e3ad637a2f9af3ed7284e77f45b83e600 100644 (file)
@@ -270,8 +270,41 @@ Gitblit also supports regex matching for repository permissions.  The following
 \r
 #### No-So-Discrete Permissions (Gitblit <= v1.1.0)\r
 \r
-Prior to v1.2.0, Gitblit had two main access permission groupings:  \r
-What you were permitted to do as an anonymous user and then **RW+** for any permitted user.\r
+Prior to v1.2.0, Gitblit has two main access permission groupings:  \r
+\r
+1. what you are permitted to do as an anonymous user\r
+2. **RW+** for any permitted user\r
+\r
+#### Committer Verification\r
+\r
+<span class='label label-warning'>Experimental</span>\r
+\r
+You may optionally enable committer verification which requires that each commit be committed by the authenticated user pushing the commits.  i.e. If Bob is pushing the commits, Bob **must** be the committer of those commits.\r
+\r
+**How is this enforced?**\r
+\r
+Bob must set his *user.name* and *user.email* values for the repository to match his Gitblit user account **BEFORE** committing to his repository.\r
+<pre>\r
+[user "bob"]\r
+    displayName = Bob Jones\r
+    emailAddress = bob@somewhere.com\r
+</pre>\r
+<pre>\r
+    git config user.name "Bob Jones"\r
+    git config user.email bob@somewhere.com    \r
+</pre>\r
+or\r
+\r
+    git config user.name bob\r
+    git config user.email bob@somewhere.com    \r
+\r
+If the Gitblit account does not specify an email address, then the committer email address is ignored.  However, if the account does specify an address it must match the committer's email address.  Display name or username can be used as the committer name.\r
+\r
+All checks are case-insensitive.\r
+\r
+**What about merges?**\r
+\r
+You can not use fast-forward merges on your client when using committer verification.  You must specify *--no-ff* to ensure that a merge commit is created with your identity as the committer.  Only the first parent chain is traversed when verifying commits.\r
 \r
 ### Teams\r
 \r
index aad6463f5faddd5d168ac7a29e06bc135f7c56a4..a26b2c8afc117aededc3bac0a5895fd4e84e3895 100644 (file)
@@ -41,6 +41,7 @@ In order to fork a repository, the user account must have the *fork* permission
 - Delete branch feature (issue 121, Github/ajermakovics)\r
 - Added line links to blob view (issue 130)\r
 - Added RedmineUserService (github/mallowlabs)\r
+- Experimental support for committer verification.  Requires use of *--no-ff* when merging branches or pull requests.  See setup page for details.\r
 \r
 #### changes\r
 \r
index bf9b1ed1ce21420f1a9776cbfd1b6b63e33e1d02..36f5f35ca310b3034604900310c9a93d81fae516 100644 (file)
@@ -1257,6 +1257,7 @@ public class GitBlit implements ServletContextListener {
                                        "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));\r
                        model.authorizationControl = AuthorizationControl.fromName(getConfig(config,\r
                                        "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));\r
+                       model.verifyCommitter = getConfig(config, "verifyCommitter", false);\r
                        model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);\r
                        model.isFrozen = getConfig(config, "isFrozen", false);\r
                        model.showReadme = getConfig(config, "showReadme", false);\r
@@ -1698,6 +1699,7 @@ public class GitBlit implements ServletContextListener {
                config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);\r
                config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());\r
                config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());\r
+               config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);\r
                config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);\r
                config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);\r
                config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme);\r
index 8e2326d43ae41d1935070c46870460eacd53aa80..42d88c91f3635fac2818458e96fe8516a308bf4e 100644 (file)
@@ -28,6 +28,7 @@ import java.text.MessageFormat;
 import java.util.Collection;\r
 import java.util.Enumeration;\r
 import java.util.LinkedHashSet;\r
+import java.util.List;\r
 import java.util.Set;\r
 \r
 import javax.servlet.ServletConfig;\r
@@ -38,6 +39,7 @@ import javax.servlet.http.HttpServletRequest;
 import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;\r
 import org.eclipse.jgit.lib.PersonIdent;\r
 import org.eclipse.jgit.lib.Repository;\r
+import org.eclipse.jgit.revwalk.RevCommit;\r
 import org.eclipse.jgit.transport.PostReceiveHook;\r
 import org.eclipse.jgit.transport.PreReceiveHook;\r
 import org.eclipse.jgit.transport.ReceiveCommand;\r
@@ -48,10 +50,12 @@ import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.slf4j.Logger;\r
 import org.slf4j.LoggerFactory;\r
 \r
+import com.gitblit.Constants.AccessRestrictionType;\r
 import com.gitblit.models.RepositoryModel;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.utils.ClientLogger;\r
 import com.gitblit.utils.HttpUtils;\r
+import com.gitblit.utils.JGitUtils;\r
 import com.gitblit.utils.StringUtils;\r
 \r
 /**\r
@@ -191,10 +195,48 @@ public class GitServlet extends org.eclipse.jgit.http.server.GitServlet {
                @Override\r
                public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {\r
                        RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);\r
+                       UserModel user = getUserModel(rp);\r
+                       \r
+                       if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {\r
+                               if (StringUtils.isEmpty(user.emailAddress)) {\r
+                                       // emit warning if user does not have an email address \r
+                                       logger.warn(MessageFormat.format("Consider setting an email address for {0} ({1}) to improve committer verification.", user.getDisplayName(), user.username));\r
+                               }\r
+                               \r
+                               // Optionally enforce that the committer of the left parent chain\r
+                               // match the account being used to push the commits.\r
+                               // \r
+                               // This requires all merge commits are executed with the "--no-ff"\r
+                               // option to force a merge commit even if fast-forward is possible.\r
+                               // This ensures that the chain of left parents has the commit\r
+                               // identity of the merging user.\r
+                               for (ReceiveCommand cmd : commands) {\r
+                                       try {\r
+                                               List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());\r
+                                               for (RevCommit commit : commits) {\r
+                                                       PersonIdent committer = commit.getCommitterIdent();\r
+                                                       if (!user.is(committer.getName(), committer.getEmailAddress())) {\r
+                                                               String reason;\r
+                                                               if (StringUtils.isEmpty(user.emailAddress)) {\r
+                                                                       // account does not have en email address\r
+                                                                       reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4})", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username);\r
+                                                               } else {\r
+                                                                       // account has an email address\r
+                                                                       reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);\r
+                                                               }\r
+                                                               cmd.setResult(Result.REJECTED_OTHER_REASON, reason);\r
+                                                               break;\r
+                                                       }\r
+                                               }\r
+                                       } catch (Exception e) {\r
+                                               logger.error("Failed to verify commits were made by pushing user", e);\r
+                                       }\r
+                               }\r
+                       }\r
+                       \r
                        Set<String> scripts = new LinkedHashSet<String>();\r
                        scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));\r
                        scripts.addAll(repository.preReceiveScripts);\r
-                       UserModel user = getUserModel(rp);\r
                        runGroovy(repository, user, commands, rp, scripts);\r
                        for (ReceiveCommand cmd : commands) {\r
                                if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {\r
index 914523df65f1377b19e7a0d7eb96f2a7ceacaa5e..502f886eff279c0f9410793bf5ecc966fa1b8a8f 100644 (file)
@@ -75,6 +75,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
        public boolean allowForks;\r
        public Set<String> forks;\r
        public String originRepository;\r
+       public boolean verifyCommitter;\r
        \r
        public RepositoryModel() {\r
                this("", "", "", new Date(0));\r
index 6fe8df2ba09a3733e7f2a07cfa282ef88b3ca2c7..8b3fe82682381face045426716dfe5976953adab 100644 (file)
@@ -405,4 +405,32 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
        public int compareTo(UserModel o) {\r
                return username.compareTo(o.username);\r
        }\r
+       \r
+       /**\r
+        * Returns true if the name/email pair match this user account.\r
+        * \r
+        * @param name\r
+        * @param email\r
+        * @return true, if the name and email address match this account\r
+        */\r
+       public boolean is(String name, String email) {\r
+               // at a minimum a usename or display name must be supplied\r
+               if (StringUtils.isEmpty(name)) {\r
+                       return false;\r
+               }\r
+               boolean nameVerified = name.equalsIgnoreCase(username) || name.equalsIgnoreCase(getDisplayName());\r
+               boolean emailVerified = false;\r
+               if (StringUtils.isEmpty(emailAddress)) {\r
+                       // user account has not specified an email address\r
+                       // rely on username/displayname verification\r
+                       emailVerified = true;\r
+               } else {\r
+                       // user account has specified an email address\r
+                       // require email address verification\r
+                       if (!StringUtils.isEmpty(email)) {\r
+                               emailVerified = email.equalsIgnoreCase(emailAddress);\r
+                       }\r
+               }\r
+               return nameVerified && emailVerified;\r
+       }\r
 }\r
index d18dcf11f4fa71f5641bdaac0cc449c7ab029d7a..e786d311467ce4acda42624987ae11b8b8fb0560 100644 (file)
@@ -341,4 +341,6 @@ gb.preparingFork = preparing your fork...
 gb.isFork = is fork\r
 gb.canCreate = can create\r
 gb.canCreateDescription = can create personal repositories\r
-gb.illegalPersonalRepositoryLocation = your personal repository must be located at \"{0}\"
\ No newline at end of file
+gb.illegalPersonalRepositoryLocation = your personal repository must be located at \"{0}\"\r
+gb.verifyCommitter = verify committer\r
+gb.verifyCommitterDescription = require committer identity to match pushing Gitblt user account (all merges require "--no-ff" to enforce committer identity)
\ No newline at end of file
index 5869917f0b8f30f936ffe5dfdbf8b95ba8cb9c55..20b77e529e61b3afa457b2365fe4e2fcc7f27638 100644 (file)
                                <tr><th colspan="2"><hr/></th></tr>\r
                                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="16" /> &nbsp;<span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>\r
                                <tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="17" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>\r
+                               <tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="18" /> &nbsp;<span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span></label></td></tr>\r
                                <tr><th colspan="2"><hr/></th></tr>\r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>\r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>\r
                                <tr><td colspan="2"><h3><wicket:message key="gb.federation"></wicket:message> &nbsp;<small><wicket:message key="gb.federationRepositoryDescription"></wicket:message></small></h3></td></tr>    \r
-                               <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="18" /></td></tr>\r
+                               <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="19" /></td></tr>\r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>\r
                                <tr><td colspan="2"><h3><wicket:message key="gb.search"></wicket:message> &nbsp;<small><wicket:message key="gb.indexedBranchesDescription"></wicket:message></small></h3></td></tr>     \r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.indexedBranches"></wicket:message></th><td style="padding:2px;"><span wicket:id="indexedBranches"></span></td></tr>\r
index 8176c28b2af8ae3b1039698b51b248d7d5e871b0..a2dad104749902ff75c46d20373a9a1fc1031919 100644 (file)
@@ -406,7 +406,7 @@ public class EditRepositoryPage extends RootSubPage {
                form.add(new DropDownChoice<FederationStrategy>("federationStrategy", federationStrategies,\r
                                new FederationTypeRenderer()));\r
                form.add(new CheckBox("useTickets"));\r
-               form.add(new CheckBox("useDocs"));              \r
+               form.add(new CheckBox("useDocs"));\r
                form.add(new CheckBox("showRemoteBranches"));\r
                form.add(new CheckBox("showReadme"));\r
                form.add(new CheckBox("skipSizeCalculation"));\r
@@ -423,6 +423,8 @@ public class EditRepositoryPage extends RootSubPage {
                group.add(allowNamed);\r
                form.add(group);\r
                                \r
+               form.add(new CheckBox("verifyCommitter"));\r
+\r
                form.add(usersPalette);\r
                form.add(teamsPalette);\r
                form.add(federationSetsPalette);\r
index a188f180f94b4716c95dc7e25e30301a3905421e..1c9bbd00355d6b0c721250d3af379308f1dcc59a 100644 (file)
@@ -68,6 +68,38 @@ public class GitBlitTest {
                assertNotNull(GitBlit.self().getRepositoryModel(user, repository));\r
                assertTrue(GitBlit.self().getRepositoryModels(user).size() > 0);\r
        }\r
+       \r
+       @Test\r
+       public void testUserModelVerification() throws Exception {\r
+               UserModel user = new UserModel("james");\r
+               user.displayName = "James Moger";\r
+               \r
+               assertTrue(user.is("James", null));\r
+               assertTrue(user.is("James", ""));\r
+               assertTrue(user.is("JaMeS", "anything"));\r
+               \r
+               assertTrue(user.is("james moger", null));\r
+               assertTrue(user.is("james moger", ""));\r
+               assertTrue(user.is("james moger", "anything"));\r
+               \r
+               assertFalse(user.is("joe", null));\r
+               assertFalse(user.is("joe", ""));\r
+               assertFalse(user.is("joe", "anything"));\r
+\r
+               // specify email address which results in address verification\r
+               user.emailAddress = "something";\r
+\r
+               assertFalse(user.is("James", null));\r
+               assertFalse(user.is("James", ""));\r
+               assertFalse(user.is("JaMeS", "anything"));\r
+               \r
+               assertFalse(user.is("james moger", null));\r
+               assertFalse(user.is("james moger", ""));\r
+               assertFalse(user.is("james moger", "anything"));\r
+\r
+               assertTrue(user.is("JaMeS", user.emailAddress));\r
+               assertTrue(user.is("JaMeS mOgEr", user.emailAddress));\r
+       }\r
 \r
        @Test\r
        public void testAccessRestrictionTypes() throws Exception {\r
index 52dddc40815d1e19991d991f03517532f1521839..4342386e8f83c4f8ec5424a3c41f3020ffbac46d 100644 (file)
@@ -248,6 +248,112 @@ public class GitServletTest {
                GitBlitSuite.close(git);\r
        }\r
 \r
+       @Test\r
+       public void testCommitterVerification() throws Exception {\r
+               UserModel user = new UserModel("james");\r
+               user.password = "james";\r
+\r
+               // account only uses account name to verify\r
+               testCommitterVerification(user, user.username, null, true);\r
+               // committer email address is ignored because account does not specify email\r
+               testCommitterVerification(user, user.username, "something", true);\r
+               // completely different committer\r
+               testCommitterVerification(user, "joe", null, false);\r
+\r
+               // test display name verification\r
+               user.displayName = "James Moger";\r
+               testCommitterVerification(user, user.displayName, null, true);\r
+               testCommitterVerification(user, user.displayName, "something", true);\r
+               testCommitterVerification(user, "joe", null, false);\r
+               \r
+               // test email address verification\r
+               user.emailAddress = "something";\r
+               testCommitterVerification(user, user.displayName, null, false);\r
+               testCommitterVerification(user, user.displayName, "somethingelse", false);\r
+               testCommitterVerification(user, user.displayName, user.emailAddress, true);\r
+               \r
+               // use same email address but with different committer\r
+               testCommitterVerification(user, "joe", "somethingelse", false);\r
+       }\r
+       \r
+       private void testCommitterVerification(UserModel user, String displayName, String emailAddress, boolean expectedSuccess) throws Exception {\r
+               \r
+               if (GitBlit.self().getUserModel(user.username) != null) {\r
+                       GitBlit.self().deleteUser(user.username);\r
+               }\r
+               \r
+               CredentialsProvider cp = new UsernamePasswordCredentialsProvider(user.username, user.password);\r
+               \r
+               // fork from original to a temporary bare repo\r
+               File verification = new File(GitBlitSuite.REPOSITORIES, "refchecks/verify-committer.git");\r
+               if (verification.exists()) {\r
+                       FileUtils.delete(verification, FileUtils.RECURSIVE);\r
+               }\r
+               CloneCommand clone = Git.cloneRepository();\r
+               clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url));\r
+               clone.setDirectory(verification);\r
+               clone.setBare(true);\r
+               clone.setCloneAllBranches(true);\r
+               clone.setCredentialsProvider(cp);\r
+               GitBlitSuite.close(clone.call());\r
+               \r
+               // require push permissions and committer verification\r
+               RepositoryModel model = GitBlit.self().getRepositoryModel("refchecks/verify-committer.git");\r
+               model.authorizationControl = AuthorizationControl.NAMED;\r
+               model.accessRestriction = AccessRestrictionType.PUSH;\r
+               model.verifyCommitter = true;\r
+               \r
+               // grant user push permission\r
+               user.setRepositoryPermission(model.name, AccessPermission.PUSH);\r
+               \r
+               GitBlit.self().updateUserModel(user.username, user, true);\r
+               GitBlit.self().updateRepositoryModel(model.name, model, false);\r
+\r
+               // clone temp bare repo to working copy\r
+               File local = new File(GitBlitSuite.REPOSITORIES, "refchecks/verify-wc");\r
+               if (local.exists()) {\r
+                       FileUtils.delete(local, FileUtils.RECURSIVE);\r
+               }\r
+               clone = Git.cloneRepository();\r
+               clone.setURI(MessageFormat.format("{0}/git/{1}", url, model.name));\r
+               clone.setDirectory(local);\r
+               clone.setBare(false);\r
+               clone.setCloneAllBranches(true);\r
+               clone.setCredentialsProvider(cp);\r
+               GitBlitSuite.close(clone.call());\r
+               \r
+               Git git = Git.open(local);\r
+               \r
+               // force an identity which may or may not match the account's identity\r
+               git.getRepository().getConfig().setString("user", null, "name", displayName);\r
+               git.getRepository().getConfig().setString("user", null, "email", emailAddress);\r
+               git.getRepository().getConfig().save();\r
+               \r
+               // commit a file and push it\r
+               File file = new File(local, "PUSHCHK");\r
+               OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);\r
+               BufferedWriter w = new BufferedWriter(os);\r
+               w.write("// " + new Date().toString() + "\n");\r
+               w.close();\r
+               git.add().addFilepattern(file.getName()).call();\r
+               git.commit().setMessage("push test").call();\r
+               Iterable<PushResult> results = git.push().setCredentialsProvider(cp).setRemote("origin").call();\r
+               \r
+               for (PushResult result : results) {\r
+                       RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");\r
+                       Status status = ref.getStatus();\r
+                       if (expectedSuccess) {\r
+                               assertTrue("Verification failed! User was NOT able to push commit! " + status.name(), Status.OK.equals(status));\r
+                       } else {\r
+                               assertTrue("Verification failed! User was able to push commit! " + status.name(), Status.REJECTED_OTHER_REASON.equals(status));\r
+                       }\r
+               }\r
+               \r
+               GitBlitSuite.close(git);\r
+               // close serving repository\r
+               GitBlitSuite.close(verification);\r
+       }\r
+\r
        @Test\r
        public void testBlockClone() throws Exception {\r
                testRefChange(AccessPermission.VIEW, null, null, null);\r