\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
- 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
"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
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
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
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
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
@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
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
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
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
<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" /> <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" /> <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" /> <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> <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> <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
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
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
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
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