* 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.11.0-rc1
@@ -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 | |||
} | |||
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 | |||
@@ -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 | |||
} |