* 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>tags/v1.21.12.1
| @@ -39,6 +39,9 @@ ACCESS_CONTROL_ALLOW_ORIGIN = | |||||
| USE_COMPAT_SSH_URI = false | USE_COMPAT_SSH_URI = false | ||||
| ; Close issues as long as a commit on any branch marks it as fixed | ; Close issues as long as a commit on any branch marks it as fixed | ||||
| DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false | 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] | [repository.editor] | ||||
| ; List of file extensions for which lines should be wrapped in the CodeMirror 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 | default is not to present. **WARNING**: This maybe harmful to you website if you do not | ||||
| give it a right value. | 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. | - `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`) | ### 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) | rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| mediaTest(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) { | t.Run("SSH", func(t *testing.T) { | ||||
| defer PrintCurrentTest(t)() | defer PrintCurrentTest(t)() | ||||
| @@ -113,6 +115,8 @@ func testGit(t *testing.T, u *url.URL) { | |||||
| rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) | ||||
| mediaTest(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 | AccessControlAllowOrigin string | ||||
| UseCompatSSHURI bool | UseCompatSSHURI bool | ||||
| DefaultCloseIssuesViaCommitsInAnyBranch bool | DefaultCloseIssuesViaCommitsInAnyBranch bool | ||||
| EnablePushCreateUser bool | |||||
| EnablePushCreateOrg bool | |||||
| // Repository editor settings | // Repository editor settings | ||||
| Editor struct { | Editor struct { | ||||
| @@ -89,6 +91,8 @@ var ( | |||||
| AccessControlAllowOrigin: "", | AccessControlAllowOrigin: "", | ||||
| UseCompatSSHURI: false, | UseCompatSSHURI: false, | ||||
| DefaultCloseIssuesViaCommitsInAnyBranch: false, | DefaultCloseIssuesViaCommitsInAnyBranch: false, | ||||
| EnablePushCreateUser: false, | |||||
| EnablePushCreateOrg: false, | |||||
| // Repository editor settings | // Repository editor settings | ||||
| Editor: struct { | Editor: struct { | ||||
| @@ -14,6 +14,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/private" | "code.gitea.io/gitea/modules/private" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| repo_service "code.gitea.io/gitea/services/repository" | |||||
| "gitea.com/macaron/macaron" | "gitea.com/macaron/macaron" | ||||
| ) | ) | ||||
| @@ -98,44 +99,44 @@ func ServCommand(ctx *macaron.Context) { | |||||
| } | } | ||||
| // Now get the Repository and set the results section | // Now get the Repository and set the results section | ||||
| repoExist := true | |||||
| repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName) | repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName) | ||||
| if err != nil { | if err != nil { | ||||
| if models.IsErrRepoNotExist(err) { | 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, | "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 | 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 | |||||
| } | |||||
| if repoExist { | |||||
| repo.OwnerName = ownerName | |||||
| results.RepoID = repo.ID | |||||
| // 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 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 | // Get the Public Key represented by the keyID | ||||
| @@ -161,6 +162,16 @@ func ServCommand(ctx *macaron.Context) { | |||||
| results.KeyID = key.ID | results.KeyID = key.ID | ||||
| results.UserID = key.OwnerID | 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 | // 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 | // 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 | // 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 | // 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{}{ | ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | ||||
| "results": results, | "results": results, | ||||
| "type": "ErrRepoIsArchived", | "type": "ErrRepoIsArchived", | ||||
| @@ -230,7 +241,7 @@ func ServCommand(ctx *macaron.Context) { | |||||
| } | } | ||||
| // Permissions checking: | // 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 key.Type == models.KeyTypeDeploy { | ||||
| if deployKey.Mode < mode { | if deployKey.Mode < mode { | ||||
| ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ | 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 | // Finally if we're trying to touch the wiki we should init it | ||||
| if results.IsWiki { | if results.IsWiki { | ||||
| if err = repo.InitWiki(); err != nil { | if err = repo.InitWiki(); err != nil { | ||||
| @@ -28,6 +28,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/process" | "code.gitea.io/gitea/modules/process" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
| repo_service "code.gitea.io/gitea/services/repository" | |||||
| ) | ) | ||||
| // HTTP implmentation git smart HTTP protocol | // HTTP implmentation git smart HTTP protocol | ||||
| @@ -100,29 +101,29 @@ func HTTP(ctx *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| repoExist := true | |||||
| repo, err := models.GetRepositoryByName(owner.ID, reponame) | repo, err := models.GetRepositoryByName(owner.ID, reponame) | ||||
| if err != nil { | if err != nil { | ||||
| if models.IsErrRepoNotExist(err) { | 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) | context.RedirectToRepo(ctx, redirectRepoID) | ||||
| } else { | |||||
| ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err) | |||||
| return | |||||
| } | } | ||||
| repoExist = false | |||||
| } else { | } else { | ||||
| ctx.ServerError("GetRepositoryByName", err) | ctx.ServerError("GetRepositoryByName", err) | ||||
| return | |||||
| } | } | ||||
| return | |||||
| } | } | ||||
| // Don't allow pushing if the repo is archived | // 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.") | ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.") | ||||
| return | return | ||||
| } | } | ||||
| // Only public pull don't need auth. | // Only public pull don't need auth. | ||||
| isPublicPull := !repo.IsPrivate && isPull | |||||
| isPublicPull := repoExist && !repo.IsPrivate && isPull | |||||
| var ( | var ( | ||||
| askAuth = !isPublicPull || setting.Service.RequireSignInView | askAuth = !isPublicPull || setting.Service.RequireSignInView | ||||
| authUser *models.User | 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{ | environ = []string{ | ||||
| @@ -264,7 +267,6 @@ func HTTP(ctx *context.Context) { | |||||
| models.EnvRepoName + "=" + reponame, | models.EnvRepoName + "=" + reponame, | ||||
| models.EnvPusherName + "=" + authUser.Name, | models.EnvPusherName + "=" + authUser.Name, | ||||
| models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID), | models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID), | ||||
| models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID), | |||||
| models.EnvIsDeployKey + "=false", | 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 | w := ctx.Resp | ||||
| r := ctx.Req.Request | r := ctx.Req.Request | ||||
| cfg := &serviceConfig{ | cfg := &serviceConfig{ | ||||
| @@ -5,6 +5,8 @@ | |||||
| package repository | package repository | ||||
| import ( | import ( | ||||
| "fmt" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
| @@ -54,3 +56,28 @@ func DeleteRepository(doer *models.User, repo *models.Repository) error { | |||||
| return nil | 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 | |||||
| } | |||||