1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-11-04 08:17:24 -05:00

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:
John Olheiser 2019-12-14 20:49:52 -06:00 committed by Lunny Xiao
parent 47c24be293
commit 6715677b2b
7 changed files with 219 additions and 51 deletions

View File

@ -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

View File

@ -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`)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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,16 +99,12 @@ 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
"results": results, } else {
"type": "ErrRepoNotExist",
"err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
})
return
}
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"results": results, "results": results,
@ -116,6 +113,9 @@ func ServCommand(ctx *macaron.Context) {
}) })
return return
} }
}
if repoExist {
repo.OwnerName = ownerName repo.OwnerName = ownerName
results.RepoID = repo.ID results.RepoID = repo.ID
@ -137,6 +137,7 @@ func ServCommand(ctx *macaron.Context) {
}) })
return return
} }
}
// Get the Public Key represented by the keyID // Get the Public Key represented by the keyID
key, err := models.GetPublicKeyByID(keyID) key, err := models.GetPublicKeyByID(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 {

View File

@ -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 redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
if err == nil {
context.RedirectToRepo(ctx, redirectRepoID) context.RedirectToRepo(ctx, redirectRepoID)
} else { return
ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err)
} }
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,6 +244,7 @@ func HTTP(ctx *context.Context) {
} }
} }
if repoExist {
perm, err := models.GetUserRepoPermission(repo, authUser) perm, err := models.GetUserRepoPermission(repo, authUser)
if err != nil { if err != nil {
ctx.ServerError("GetUserRepoPermission", err) ctx.ServerError("GetUserRepoPermission", err)
@ -258,13 +260,13 @@ func HTTP(ctx *context.Context) {
ctx.HandleText(http.StatusForbidden, "mirror repository is read-only") ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
return return
} }
}
environ = []string{ environ = []string{
models.EnvRepoUsername + "=" + username, models.EnvRepoUsername + "=" + username,
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{

View File

@ -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
}