]> source.dussan.org Git - gitblit.git/commitdiff
Implemented optional create-on-push
authorJames Moger <james.moger@gitblit.com>
Mon, 22 Oct 2012 02:04:35 +0000 (22:04 -0400)
committerJames Moger <james.moger@gitblit.com>
Mon, 22 Oct 2012 02:04:35 +0000 (22:04 -0400)
If this feature is enabled then permitted users can automatically create a
repository when pushing to one that does not yet exist.  Permitted users
are administrators and any user with the CREATE role.

If the pushing account is an administrator, the created repository may be
located in any folder/project space.  This reposiory will inherit the server's
default access restriction and authorization control.  The repository owner
will be the pushing account.

If the pushing account is a regular user with the CREATE role, the repository
can only be located in the account's personal folder (~username/myrepo.git).
This repository will be VIEW restricted and accessible by NAMED users.  The
repository owner will be the pushing account.

distrib/gitblit.properties
docs/01_features.mkd
docs/04_releases.mkd
docs/05_roadmap.mkd
src/com/gitblit/AccessRestrictionFilter.java
src/com/gitblit/DownloadZipFilter.java
src/com/gitblit/GitFilter.java
src/com/gitblit/PagesFilter.java
src/com/gitblit/models/UserModel.java
tests/com/gitblit/tests/GitServletTest.java

index fe7692be3140b2df122af0a89d418b35bbdcaf5e..89a7c2f36ff55c644fb8385c4aa85792a6c7f81c 100644 (file)
@@ -76,6 +76,20 @@ git.enableGitServlet = true
 # SINCE 0.9.0\r
 git.onlyAccessBareRepositories = false\r
 \r
+# Allow an authenticated user to create a destination repository on a push if\r
+# the repository does not already exist.\r
+#\r
+# Administrator accounts can create a repository in any project.\r
+# These repositories are created with the default access restriction and authorization\r
+# control values.  The pushing account is set as the owner.\r
+#\r
+# Non-administrator accounts with the CREATE role may create personal repositories.\r
+# These repositories are created as VIEW restricted for NAMED users.\r
+# The pushing account is set as the owner.\r
+#\r
+# SINCE 1.2.0\r
+git.allowCreateOnPush = true\r
+\r
 # The default access restriction for new repositories.\r
 # Valid values are NONE, PUSH, CLONE, VIEW\r
 #  NONE = anonymous view, clone, & push\r
index e1c0afa477913c3a6036a49915a336fde50fea74..e9e7726c6ebdc584df8c6cfa1fd893a4ca840355 100644 (file)
@@ -16,6 +16,7 @@
     - **RW+** (clone and push with ref creation, deletion, rewind)\r
 - Optional feature to allow users to create personal repositories\r
 - Optional feature to fork a repository to a personal repository\r
+- Optional feature to create a repository on push\r
 - Ability to federate with one or more other Gitblit instances\r
 - RSS/JSON RPC interface\r
 - Java/Swing Gitblit Manager tool \r
index 0cf060e47dd9525062040e87124055edaa2a9d5e..1ac7de3a56a524cca434c31d1170dbaf8aed177a 100644 (file)
@@ -39,6 +39,8 @@ This allows you to specify a permission like `RW:mygroup/[A-Za-z0-9-~_\\./]+` to
 Personal repositories can be created by accounts with the *create* permission and are stored in *git.repositoriesFolder/~username*.  Each user with personal repositories will have a user page, something like the GitHub profile page.  Personal repositories have all the same features as common repositories, except personal repositories can be renamed by their owner.\r
 - Added support for server-side forking of a repository to a personal repository (issue 137)  \r
 In order to fork a repository, the user account must have the *fork* permission **and** the repository must *allow forks*.  The clone inherits the access list of its origin.  i.e. if Team A has clone access to the origin repository, then by default Team A also has clone access to the fork.  This is to facilitate collaboration.  The fork owner may change access to the fork and add/remove users/teams, etc as required <u>however</u> it should be noted that all personal forks will be enumerated in the fork network regardless of access view restrictions.  If you really must have an invisible fork, the clone it locally, create a new repository for your invisible fork, and push it back to Gitblit.\r
+- Added optional *create-on-push* support  \r
+    **New:** *git.allowCreateOnPush=true*\r
 - Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.\r
 - Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)\r
 - Delete branch feature (issue 121, Github/ajermakovics)\r
index 562cc7e7626e801fe35491a83fdecd7d532bf7cc..14ad8c196d41586e3720083dc3c5d77253879ffd 100644 (file)
@@ -13,10 +13,6 @@ This list is volatile.
 ### TODO (medium priority)\r
 \r
 * Gitblit: editable settings page in GO/WAR\r
-* Gitblit: investigate create-repository-on-push.\r
-    * Maybe a new user role to allow this?\r
-    * Maybe a server setting to disable this completely?\r
-    * Pusher/Creator becomes repository owner and can then manipulate access lists, etc?\r
 * Gitblit: Clone Repository feature (issue 5)\r
     * optional scheduled pulls\r
     * optional automatic push to origin/remotes?\r
index 3a104813aa1ef973cbf1a7af81629184561d05f0..78d33d2183551c4dc591d48f66f7c94e7dcfb20c 100644 (file)
@@ -61,6 +61,13 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
         */\r
        protected abstract String getUrlRequestAction(String url);\r
 \r
+       /**\r
+        * Determine if a non-existing repository can be created using this filter.\r
+        *  \r
+        * @return true if the filter allows repository creation\r
+        */\r
+       protected abstract boolean isCreationAllowed();\r
+       \r
        /**\r
         * Determine if the action may be executed on the repository.\r
         * \r
@@ -90,6 +97,18 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
         */\r
        protected abstract boolean canAccess(RepositoryModel repository, UserModel user, String action);\r
 \r
+       /**\r
+        * Allows a filter to create a repository, if one does not exist.\r
+        * \r
+        * @param user\r
+        * @param repository\r
+        * @param action\r
+        * @return the repository model, if it is created, null otherwise\r
+        */\r
+       protected RepositoryModel createRepository(UserModel user, String repository, String action) {\r
+               return null;\r
+       }\r
+       \r
        /**\r
         * doFilter does the actual work of preprocessing the request to ensure that\r
         * the user may proceed.\r
@@ -111,14 +130,33 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
                String fullSuffix = fullUrl.substring(repository.length());\r
                String urlRequestType = getUrlRequestAction(fullSuffix);\r
 \r
+               UserModel user = getUser(httpRequest);\r
+\r
                // Load the repository model\r
                RepositoryModel model = GitBlit.self().getRepositoryModel(repository);\r
                if (model == null) {\r
-                       // repository not found. send 404.\r
-                       logger.info(MessageFormat.format("ARF: {0} ({1})", fullUrl,\r
-                                       HttpServletResponse.SC_NOT_FOUND));\r
-                       httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);\r
-                       return;\r
+                       if (isCreationAllowed()) {\r
+                               if (user == null) {\r
+                                       // challenge client to provide credentials for creation. send 401.\r
+                                       if (GitBlit.isDebugMode()) {\r
+                                               logger.info(MessageFormat.format("ARF: CREATE CHALLENGE {0}", fullUrl));\r
+                                       }\r
+                                       httpResponse.setHeader("WWW-Authenticate", CHALLENGE);\r
+                                       httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);\r
+                                       return;\r
+                               } else {\r
+                                       // see if we can create a repository for this request\r
+                                       model = createRepository(user, repository, urlRequestType);\r
+                               }\r
+                       }\r
+                       \r
+                       if (model == null) {\r
+                               // repository not found. send 404.\r
+                               logger.info(MessageFormat.format("ARF: {0} ({1})", fullUrl,\r
+                                               HttpServletResponse.SC_NOT_FOUND));\r
+                               httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);\r
+                               return;\r
+                       }\r
                }\r
                \r
                // Confirm that the action may be executed on the repository\r
@@ -139,7 +177,6 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
                // Gitblit must conditionally authenticate users per-repository so just\r
                // enabling http.receivepack is insufficient.\r
                AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest);\r
-               UserModel user = getUser(httpRequest);\r
                if (user != null) {\r
                        authenticatedRequest.setUser(user);\r
                }\r
index 225e5e115ab7dec192886bf67a469df2aa06dd4e..90a764930a65c29afd1d5c110a794611d1121972 100644 (file)
@@ -56,6 +56,16 @@ public class DownloadZipFilter extends AccessRestrictionFilter {
                return "DOWNLOAD";\r
        }\r
 \r
+       /**\r
+        * Determine if a non-existing repository can be created using this filter.\r
+        *  \r
+        * @return true if the filter allows repository creation\r
+        */\r
+       @Override\r
+       protected boolean isCreationAllowed() {\r
+               return false;\r
+       }\r
+\r
        /**\r
         * Determine if the action may be executed on the repository.\r
         * \r
index cfe4fe3f8ee57170585bb51f3d27ebdb5ccc13c6..c09b0d20388b0c38e26d53f316c88e374125f22b 100644 (file)
@@ -18,6 +18,7 @@ package com.gitblit;
 import java.text.MessageFormat;\r
 \r
 import com.gitblit.Constants.AccessRestrictionType;\r
+import com.gitblit.Constants.AuthorizationControl;\r
 import com.gitblit.models.RepositoryModel;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.utils.StringUtils;\r
@@ -92,6 +93,16 @@ public class GitFilter extends AccessRestrictionFilter {
                return null;\r
        }\r
        \r
+       /**\r
+        * Determine if a non-existing repository can be created using this filter.\r
+        *  \r
+        * @return true if the server allows repository creation on-push\r
+        */\r
+       @Override\r
+       protected boolean isCreationAllowed() {\r
+               return GitBlit.getBoolean(Keys.git.allowCreateOnPush, true);\r
+       }\r
+       \r
        /**\r
         * Determine if the repository can receive pushes.\r
         * \r
@@ -170,4 +181,50 @@ public class GitFilter extends AccessRestrictionFilter {
                }\r
                return true;\r
        }\r
+       \r
+       /**\r
+        * An authenticated user with the CREATE role can create a repository on\r
+        * push.\r
+        * \r
+        * @param user\r
+        * @param repository\r
+        * @param action\r
+        * @return the repository model, if it is created, null otherwise\r
+        */\r
+       @Override\r
+       protected RepositoryModel createRepository(UserModel user, String repository, String action) {\r
+               boolean isPush = !StringUtils.isEmpty(action) && gitReceivePack.equals(action);\r
+               if (isPush) {\r
+                       if (user.canCreateOnPush(repository)) {\r
+                               // user is pushing to a new repository\r
+                               RepositoryModel model = new RepositoryModel();\r
+                               model.name = repository;\r
+                               model.owner = user.username;\r
+                               model.projectPath = StringUtils.getFirstPathElement(repository);\r
+                               if (model.isUsersPersonalRepository(user.username)) {\r
+                                       // personal repository, default to private for user\r
+                                       model.authorizationControl = AuthorizationControl.NAMED;\r
+                                       model.accessRestriction = AccessRestrictionType.VIEW;\r
+                               } else {\r
+                                       // common repository, user default server settings\r
+                                       model.authorizationControl = AuthorizationControl.fromName(GitBlit.getString(Keys.git.defaultAuthorizationControl, ""));\r
+                                       model.accessRestriction = AccessRestrictionType.fromName(GitBlit.getString(Keys.git.defaultAccessRestriction, ""));\r
+                               }\r
+\r
+                               // create the repository\r
+                               try {\r
+                                       GitBlit.self().updateRepositoryModel(repository, model, true);\r
+                                       logger.info(MessageFormat.format("{0} created {1} ON-PUSH", user.username, repository));\r
+                                       return GitBlit.self().getRepositoryModel(repository);\r
+                               } catch (GitBlitException e) {\r
+                                       logger.error(MessageFormat.format("{0} failed to create repository {1} ON-PUSH!", user.username, repository), e);\r
+                               }\r
+                       } else {\r
+                               logger.warn(MessageFormat.format("{0} is not permitted to create repository {1} ON-PUSH!", user.username, repository));\r
+                       }\r
+               }\r
+               \r
+               // repository could not be created or action was not a push\r
+               return null;\r
+       }\r
 }\r
index 11cdfa5687ecadce62fa5ba67e2a6794db331274..f88624e108199401960c0bb30529b90fc9818504 100644 (file)
@@ -76,6 +76,16 @@ public class PagesFilter extends AccessRestrictionFilter {
                return "VIEW";\r
        }\r
 \r
+       /**\r
+        * Determine if a non-existing repository can be created using this filter.\r
+        *  \r
+        * @return true if the filter allows repository creation\r
+        */\r
+       @Override\r
+       protected boolean isCreationAllowed() {\r
+               return false;\r
+       }\r
+\r
        /**\r
         * Determine if the action may be executed on the repository.\r
         * \r
index fc9cbfba54064c30ec6583d5e6c0d748dd0f3c8d..7995f7e4a3373396c50571dc2315a70edd484781 100644 (file)
@@ -364,6 +364,28 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
                }\r
                return false;\r
        }\r
+       \r
+       /**\r
+        * Returns true if the user is allowed to create the specified repository\r
+        * on-push if the repository does not already exist.\r
+        * \r
+        * @param repository\r
+        * @return true if the user can create the repository\r
+        */\r
+       public boolean canCreateOnPush(String repository) {\r
+               if (canAdmin()) {\r
+                       // admins can create any repository\r
+                       return true;\r
+               }\r
+               if (canCreate) {\r
+                       String projectPath = StringUtils.getFirstPathElement(repository);\r
+                       if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username)) {\r
+                               // personal repository\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
 \r
        public boolean isTeamMember(String teamname) {\r
                for (TeamModel team : teams) {\r
index 4342386e8f83c4f8ec5424a3c41f3020ffbac46d..17d462ac61ef42ccd64bd4f582fb3fd9358fc5fe 100644 (file)
@@ -1,5 +1,6 @@
 package com.gitblit.tests;\r
 \r
+import static org.junit.Assert.assertEquals;\r
 import static org.junit.Assert.assertFalse;\r
 import static org.junit.Assert.assertTrue;\r
 \r
@@ -32,6 +33,7 @@ import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.AccessRestrictionType;\r
 import com.gitblit.Constants.AuthorizationControl;\r
 import com.gitblit.GitBlit;\r
+import com.gitblit.Keys;\r
 import com.gitblit.models.RepositoryModel;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.utils.JGitUtils;\r
@@ -577,4 +579,108 @@ public class GitServletTest {
 \r
                GitBlit.self().deleteUser(user.username);\r
        }\r
+       \r
+       @Test\r
+       public void testCreateOnPush() throws Exception {\r
+               testCreateOnPush(false, false);\r
+               testCreateOnPush(true, false);\r
+               testCreateOnPush(false, true);\r
+       }\r
+       \r
+       private void testCreateOnPush(boolean canCreate, boolean canAdmin) throws Exception {\r
+\r
+               UserModel user = new UserModel("sampleuser");\r
+               user.password = user.username;\r
+               \r
+               if (GitBlit.self().getUserModel(user.username) != null) {\r
+                       GitBlit.self().deleteUser(user.username);\r
+               }\r
+               \r
+               user.canCreate = canCreate;\r
+               user.canAdmin = canAdmin;\r
+               \r
+               GitBlit.self().updateUserModel(user.username, user, true);\r
+\r
+               CredentialsProvider cp = new UsernamePasswordCredentialsProvider(user.username, user.password);\r
+               \r
+               // fork from original to a temporary bare repo\r
+               File tmpFolder = File.createTempFile("gitblit", "").getParentFile();\r
+               File createCheck = new File(tmpFolder, "ticgit.git");\r
+               if (createCheck.exists()) {\r
+                       FileUtils.delete(createCheck, FileUtils.RECURSIVE);\r
+               }\r
+               \r
+               File personalRepo = new File(GitBlitSuite.REPOSITORIES, MessageFormat.format("~{0}/ticgit.git", user.username));\r
+               if (personalRepo.exists()) {\r
+                       FileUtils.delete(personalRepo, FileUtils.RECURSIVE);\r
+               }\r
+\r
+               File projectRepo = new File(GitBlitSuite.REPOSITORIES, "project/ticgit.git");\r
+               if (projectRepo.exists()) {\r
+                       FileUtils.delete(projectRepo, FileUtils.RECURSIVE);\r
+               }\r
+\r
+               CloneCommand clone = Git.cloneRepository();\r
+               clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url));\r
+               clone.setDirectory(createCheck);\r
+               clone.setBare(true);\r
+               clone.setCloneAllBranches(true);\r
+               clone.setCredentialsProvider(cp);\r
+               Git git = clone.call();\r
+               \r
+               // add a personal repository remote and a project remote\r
+               git.getRepository().getConfig().setString("remote", "user", "url", MessageFormat.format("{0}/git/~{1}/ticgit.git", url, user.username));\r
+               git.getRepository().getConfig().setString("remote", "project", "url", MessageFormat.format("{0}/git/project/ticgit.git", url));\r
+               git.getRepository().getConfig().save();\r
+\r
+               // push to non-existent user repository\r
+               try {\r
+                       Iterable<PushResult> results = git.push().setRemote("user").setPushAll().setCredentialsProvider(cp).call();\r
+\r
+                       for (PushResult result : results) {\r
+                               RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");\r
+                               Status status = ref.getStatus();\r
+                               assertTrue("User failed to create repository?! " + status.name(), Status.OK.equals(status));\r
+                       }\r
+\r
+                       assertTrue("User canAdmin:" + user.canAdmin + " canCreate:" + user.canCreate, user.canAdmin || user.canCreate);\r
+                       \r
+                       // confirm default personal repository permissions\r
+                       RepositoryModel model = GitBlit.self().getRepositoryModel(MessageFormat.format("~{0}/ticgit.git", user.username));\r
+                       assertEquals("Unexpected owner", user.username, model.owner);\r
+                       assertEquals("Unexpected authorization control", AuthorizationControl.NAMED, model.authorizationControl);\r
+                       assertEquals("Unexpected access restriction", AccessRestrictionType.VIEW, model.accessRestriction);\r
+                       \r
+               } catch (GitAPIException e) {\r
+                       assertTrue(e.getMessage(), e.getMessage().contains("git-receive-pack not found"));\r
+                       assertFalse("User canAdmin:" + user.canAdmin + " canCreate:" + user.canCreate, user.canAdmin || user.canCreate);\r
+               }\r
+               \r
+               // push to non-existent project repository\r
+               try {\r
+                       Iterable<PushResult> results = git.push().setRemote("project").setPushAll().setCredentialsProvider(cp).call();\r
+                       GitBlitSuite.close(git);\r
+\r
+                       for (PushResult result : results) {\r
+                               RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");\r
+                               Status status = ref.getStatus();\r
+                               assertTrue("User failed to create repository?! " + status.name(), Status.OK.equals(status));\r
+                       }\r
+                       \r
+                       assertTrue("User canAdmin:" + user.canAdmin, user.canAdmin);\r
+                       \r
+                       // confirm default project repository permissions\r
+                       RepositoryModel model = GitBlit.self().getRepositoryModel("project/ticgit.git");\r
+                       assertEquals("Unexpected owner", user.username, model.owner);\r
+                       assertEquals("Unexpected authorization control", AuthorizationControl.fromName(GitBlit.getString(Keys.git.defaultAuthorizationControl, "NAMED")), model.authorizationControl);\r
+                       assertEquals("Unexpected access restriction", AccessRestrictionType.fromName(GitBlit.getString(Keys.git.defaultAccessRestriction, "NONE")), model.accessRestriction);\r
+\r
+               } catch (GitAPIException e) {\r
+                       assertTrue(e.getMessage(), e.getMessage().contains("git-receive-pack not found"));\r
+                       assertFalse("User canAdmin:" + user.canAdmin, user.canAdmin);\r
+               }\r
+\r
+               GitBlitSuite.close(git);\r
+               GitBlit.self().deleteUser(user.username);\r
+       }\r
 }\r