Push to create repo (#8419)
* Refactor Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add push-create to SSH serv Signed-off-by: jolheiser <john.olheiser@gmail.com> * Cannot push for another user unless admin Signed-off-by: jolheiser <john.olheiser@gmail.com> * Get owner in case admin pushes for another user Signed-off-by: jolheiser <john.olheiser@gmail.com> * Set new repo ID in result Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update to service and use new org perms Signed-off-by: jolheiser <john.olheiser@gmail.com> * Move pushCreateRepo to services Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix import order Signed-off-by: jolheiser <john.olheiser@gmail.com> * Changes for @guillep2k * Check owner (not user) in SSH * Add basic tests for created repos (private, not empty) Signed-off-by: jolheiser <john.olheiser@gmail.com>
This commit is contained in:
		
							parent
							
								
									47c24be293
								
							
						
					
					
						commit
						6715677b2b
					
				| @ -39,6 +39,9 @@ ACCESS_CONTROL_ALLOW_ORIGIN = | ||||
| USE_COMPAT_SSH_URI = false | ||||
| ; Close issues as long as a commit on any branch marks it as fixed | ||||
| DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false | ||||
| ; Allow users to push local repositories to Gitea and have them automatically created for a user or an org | ||||
| ENABLE_PUSH_CREATE_USER = false | ||||
| ENABLE_PUSH_CREATE_ORG = false | ||||
| 
 | ||||
| [repository.editor] | ||||
| ; List of file extensions for which lines should be wrapped in the CodeMirror editor | ||||
|  | ||||
| @ -66,6 +66,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | ||||
|    default is not to present. **WARNING**: This maybe harmful to you website if you do not | ||||
|    give it a right value. | ||||
| - `DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH`:  **false**: Close an issue if a commit on a non default branch marks it as closed. | ||||
| - `ENABLE_PUSH_CREATE_USER`:  **false**: Allow users to push local repositories to Gitea and have them automatically created for a user. | ||||
| - `ENABLE_PUSH_CREATE_ORG`:  **false**: Allow users to push local repositories to Gitea and have them automatically created for an org. | ||||
| 
 | ||||
| ### Repository - Pull Request (`repository.pull-request`) | ||||
| 
 | ||||
|  | ||||
| @ -75,6 +75,8 @@ func testGit(t *testing.T, u *url.URL) { | ||||
| 			rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| 			mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("PushCreate", doPushCreate(httpContext, u)) | ||||
| 	}) | ||||
| 	t.Run("SSH", func(t *testing.T) { | ||||
| 		defer PrintCurrentTest(t)() | ||||
| @ -113,6 +115,8 @@ func testGit(t *testing.T, u *url.URL) { | ||||
| 				rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| 				mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| 			}) | ||||
| 
 | ||||
| 			t.Run("PushCreate", doPushCreate(sshContext, sshURL)) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @ -408,3 +412,57 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) { | ||||
| 	return func(t *testing.T) { | ||||
| 		defer PrintCurrentTest(t)() | ||||
| 		ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme) | ||||
| 		u.Path = ctx.GitPath() | ||||
| 
 | ||||
| 		tmpDir, err := ioutil.TempDir("", ctx.Reponame) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		err = git.InitRepository(tmpDir, false) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		_, err = os.Create(filepath.Join(tmpDir, "test.txt")) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		err = git.AddChanges(tmpDir, true) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		err = git.CommitChanges(tmpDir, git.CommitChangesOptions{ | ||||
| 			Committer: &git.Signature{ | ||||
| 				Email: "user2@example.com", | ||||
| 				Name:  "User Two", | ||||
| 				When:  time.Now(), | ||||
| 			}, | ||||
| 			Author: &git.Signature{ | ||||
| 				Email: "user2@example.com", | ||||
| 				Name:  "User Two", | ||||
| 				When:  time.Now(), | ||||
| 			}, | ||||
| 			Message: fmt.Sprintf("Testing push create @ %v", time.Now()), | ||||
| 		}) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		_, err = git.NewCommand("remote", "add", "origin", u.String()).RunInDir(tmpDir) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// Push to create disabled
 | ||||
| 		setting.Repository.EnablePushCreateUser = false | ||||
| 		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir) | ||||
| 		assert.Error(t, err) | ||||
| 
 | ||||
| 		// Push to create enabled
 | ||||
| 		setting.Repository.EnablePushCreateUser = true | ||||
| 		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// Fetch repo from database
 | ||||
| 		repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.False(t, repo.IsEmpty) | ||||
| 		assert.True(t, repo.IsPrivate) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -35,6 +35,8 @@ var ( | ||||
| 		AccessControlAllowOrigin                string | ||||
| 		UseCompatSSHURI                         bool | ||||
| 		DefaultCloseIssuesViaCommitsInAnyBranch bool | ||||
| 		EnablePushCreateUser                    bool | ||||
| 		EnablePushCreateOrg                     bool | ||||
| 
 | ||||
| 		// Repository editor settings
 | ||||
| 		Editor struct { | ||||
| @ -89,6 +91,8 @@ var ( | ||||
| 		AccessControlAllowOrigin:                "", | ||||
| 		UseCompatSSHURI:                         false, | ||||
| 		DefaultCloseIssuesViaCommitsInAnyBranch: false, | ||||
| 		EnablePushCreateUser:                    false, | ||||
| 		EnablePushCreateOrg:                     false, | ||||
| 
 | ||||
| 		// Repository editor settings
 | ||||
| 		Editor: struct { | ||||
|  | ||||
| @ -14,6 +14,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/private" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| 
 | ||||
| 	"gitea.com/macaron/macaron" | ||||
| ) | ||||
| @ -98,44 +99,44 @@ func ServCommand(ctx *macaron.Context) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Now get the Repository and set the results section
 | ||||
| 	repoExist := true | ||||
| 	repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrRepoNotExist(err) { | ||||
| 			ctx.JSON(http.StatusNotFound, map[string]interface{}{ | ||||
| 			repoExist = false | ||||
| 		} else { | ||||
| 			log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "ErrRepoNotExist", | ||||
| 				"err":     fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), | ||||
| 				"type":    "InternalServerError", | ||||
| 				"err":     fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"results": results, | ||||
| 			"type":    "InternalServerError", | ||||
| 			"err":     fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	repo.OwnerName = ownerName | ||||
| 	results.RepoID = repo.ID | ||||
| 
 | ||||
| 	if repo.IsBeingCreated() { | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"results": results, | ||||
| 			"type":    "InternalServerError", | ||||
| 			"err":     "Repository is being created, you could retry after it finished", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// We can shortcut at this point if the repo is a mirror
 | ||||
| 	if mode > models.AccessModeRead && repo.IsMirror { | ||||
| 		ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | ||||
| 			"results": results, | ||||
| 			"type":    "ErrMirrorReadOnly", | ||||
| 			"err":     fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), | ||||
| 		}) | ||||
| 		return | ||||
| 	if repoExist { | ||||
| 		repo.OwnerName = ownerName | ||||
| 		results.RepoID = repo.ID | ||||
| 
 | ||||
| 		if repo.IsBeingCreated() { | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "InternalServerError", | ||||
| 				"err":     "Repository is being created, you could retry after it finished", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// We can shortcut at this point if the repo is a mirror
 | ||||
| 		if mode > models.AccessModeRead && repo.IsMirror { | ||||
| 			ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "ErrMirrorReadOnly", | ||||
| 				"err":     fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Get the Public Key represented by the keyID
 | ||||
| @ -161,6 +162,16 @@ func ServCommand(ctx *macaron.Context) { | ||||
| 	results.KeyID = key.ID | ||||
| 	results.UserID = key.OwnerID | ||||
| 
 | ||||
| 	// If repo doesn't exist, deploy key doesn't make sense
 | ||||
| 	if !repoExist && key.Type == models.KeyTypeDeploy { | ||||
| 		ctx.JSON(http.StatusNotFound, map[string]interface{}{ | ||||
| 			"results": results, | ||||
| 			"type":    "ErrRepoNotExist", | ||||
| 			"err":     fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Deploy Keys have ownerID set to 0 therefore we can't use the owner
 | ||||
| 	// So now we need to check if the key is a deploy key
 | ||||
| 	// We'll keep hold of the deploy key here for permissions checking
 | ||||
| @ -220,7 +231,7 @@ func ServCommand(ctx *macaron.Context) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't allow pushing if the repo is archived
 | ||||
| 	if mode > models.AccessModeRead && repo.IsArchived { | ||||
| 	if repoExist && mode > models.AccessModeRead && repo.IsArchived { | ||||
| 		ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | ||||
| 			"results": results, | ||||
| 			"type":    "ErrRepoIsArchived", | ||||
| @ -230,7 +241,7 @@ func ServCommand(ctx *macaron.Context) { | ||||
| 	} | ||||
| 
 | ||||
| 	// Permissions checking:
 | ||||
| 	if mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView { | ||||
| 	if repoExist && (mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView) { | ||||
| 		if key.Type == models.KeyTypeDeploy { | ||||
| 			if deployKey.Mode < mode { | ||||
| 				ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | ||||
| @ -265,6 +276,48 @@ func ServCommand(ctx *macaron.Context) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// We already know we aren't using a deploy key
 | ||||
| 	if !repoExist { | ||||
| 		owner, err := models.GetUserByName(ownerName) | ||||
| 		if err != nil { | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "InternalServerError", | ||||
| 				"err":     fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { | ||||
| 			ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "ErrForbidden", | ||||
| 				"err":     "Push to create is not enabled for organizations.", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { | ||||
| 			ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "ErrForbidden", | ||||
| 				"err":     "Push to create is not enabled for users.", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		repo, err = repo_service.PushCreateRepo(user, owner, results.RepoName) | ||||
| 		if err != nil { | ||||
| 			log.Error("pushCreateRepo: %v", err) | ||||
| 			ctx.JSON(http.StatusNotFound, map[string]interface{}{ | ||||
| 				"results": results, | ||||
| 				"type":    "ErrRepoNotExist", | ||||
| 				"err":     fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		results.RepoID = repo.ID | ||||
| 	} | ||||
| 
 | ||||
| 	// Finally if we're trying to touch the wiki we should init it
 | ||||
| 	if results.IsWiki { | ||||
| 		if err = repo.InitWiki(); err != nil { | ||||
|  | ||||
| @ -28,6 +28,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/process" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
| 
 | ||||
| // HTTP implmentation git smart HTTP protocol
 | ||||
| @ -100,29 +101,29 @@ func HTTP(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	repoExist := true | ||||
| 	repo, err := models.GetRepositoryByName(owner.ID, reponame) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrRepoNotExist(err) { | ||||
| 			redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame) | ||||
| 			if err == nil { | ||||
| 			if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil { | ||||
| 				context.RedirectToRepo(ctx, redirectRepoID) | ||||
| 			} else { | ||||
| 				ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err) | ||||
| 				return | ||||
| 			} | ||||
| 			repoExist = false | ||||
| 		} else { | ||||
| 			ctx.ServerError("GetRepositoryByName", err) | ||||
| 			return | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't allow pushing if the repo is archived
 | ||||
| 	if repo.IsArchived && !isPull { | ||||
| 	if repoExist && repo.IsArchived && !isPull { | ||||
| 		ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Only public pull don't need auth.
 | ||||
| 	isPublicPull := !repo.IsPrivate && isPull | ||||
| 	isPublicPull := repoExist && !repo.IsPrivate && isPull | ||||
| 	var ( | ||||
| 		askAuth      = !isPublicPull || setting.Service.RequireSignInView | ||||
| 		authUser     *models.User | ||||
| @ -243,20 +244,22 @@ func HTTP(ctx *context.Context) { | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		perm, err := models.GetUserRepoPermission(repo, authUser) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("GetUserRepoPermission", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if repoExist { | ||||
| 			perm, err := models.GetUserRepoPermission(repo, authUser) | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError("GetUserRepoPermission", err) | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 		if !perm.CanAccess(accessMode, unitType) { | ||||
| 			ctx.HandleText(http.StatusForbidden, "User permission denied") | ||||
| 			return | ||||
| 		} | ||||
| 			if !perm.CanAccess(accessMode, unitType) { | ||||
| 				ctx.HandleText(http.StatusForbidden, "User permission denied") | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 		if !isPull && repo.IsMirror { | ||||
| 			ctx.HandleText(http.StatusForbidden, "mirror repository is read-only") | ||||
| 			return | ||||
| 			if !isPull && repo.IsMirror { | ||||
| 				ctx.HandleText(http.StatusForbidden, "mirror repository is read-only") | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		environ = []string{ | ||||
| @ -264,7 +267,6 @@ func HTTP(ctx *context.Context) { | ||||
| 			models.EnvRepoName + "=" + reponame, | ||||
| 			models.EnvPusherName + "=" + authUser.Name, | ||||
| 			models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID), | ||||
| 			models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID), | ||||
| 			models.EnvIsDeployKey + "=false", | ||||
| 		} | ||||
| 
 | ||||
| @ -279,6 +281,25 @@ func HTTP(ctx *context.Context) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !repoExist { | ||||
| 		if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { | ||||
| 			ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.") | ||||
| 			return | ||||
| 		} | ||||
| 		if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { | ||||
| 			ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.") | ||||
| 			return | ||||
| 		} | ||||
| 		repo, err = repo_service.PushCreateRepo(authUser, owner, reponame) | ||||
| 		if err != nil { | ||||
| 			log.Error("pushCreateRepo: %v", err) | ||||
| 			ctx.Status(http.StatusNotFound) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	environ = append(environ, models.ProtectedBranchRepoID+fmt.Sprintf("=%d", repo.ID)) | ||||
| 
 | ||||
| 	w := ctx.Resp | ||||
| 	r := ctx.Req.Request | ||||
| 	cfg := &serviceConfig{ | ||||
|  | ||||
| @ -5,6 +5,8 @@ | ||||
| package repository | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/notification" | ||||
| @ -54,3 +56,28 @@ func DeleteRepository(doer *models.User, repo *models.Repository) error { | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace
 | ||||
| func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repository, error) { | ||||
| 	if !authUser.IsAdmin { | ||||
| 		if owner.IsOrganization() { | ||||
| 			if ok, err := owner.CanCreateOrgRepo(authUser.ID); err != nil { | ||||
| 				return nil, err | ||||
| 			} else if !ok { | ||||
| 				return nil, fmt.Errorf("cannot push-create repository for org") | ||||
| 			} | ||||
| 		} else if authUser.ID != owner.ID { | ||||
| 			return nil, fmt.Errorf("cannot push-create repository for another user") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	repo, err := CreateRepository(authUser, owner, models.CreateRepoOptions{ | ||||
| 		Name:      repoName, | ||||
| 		IsPrivate: true, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return repo, nil | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user