Skip to content

Commit 6715677

Browse files
jolheiserlunny
authored andcommitted
Push to create repo (#8419)
* Refactor Signed-off-by: jolheiser <[email protected]> * Add push-create to SSH serv Signed-off-by: jolheiser <[email protected]> * Cannot push for another user unless admin Signed-off-by: jolheiser <[email protected]> * Get owner in case admin pushes for another user Signed-off-by: jolheiser <[email protected]> * Set new repo ID in result Signed-off-by: jolheiser <[email protected]> * Update to service and use new org perms Signed-off-by: jolheiser <[email protected]> * Move pushCreateRepo to services Signed-off-by: jolheiser <[email protected]> * Fix import order Signed-off-by: jolheiser <[email protected]> * Changes for @guillep2k * Check owner (not user) in SSH * Add basic tests for created repos (private, not empty) Signed-off-by: jolheiser <[email protected]>
1 parent 47c24be commit 6715677

File tree

7 files changed

+218
-50
lines changed

7 files changed

+218
-50
lines changed

custom/conf/app.ini.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ ACCESS_CONTROL_ALLOW_ORIGIN =
3939
USE_COMPAT_SSH_URI = false
4040
; Close issues as long as a commit on any branch marks it as fixed
4141
DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
42+
; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
43+
ENABLE_PUSH_CREATE_USER = false
44+
ENABLE_PUSH_CREATE_ORG = false
4245

4346
[repository.editor]
4447
; List of file extensions for which lines should be wrapped in the CodeMirror editor

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
6666
default is not to present. **WARNING**: This maybe harmful to you website if you do not
6767
give it a right value.
6868
- `DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH`: **false**: Close an issue if a commit on a non default branch marks it as closed.
69+
- `ENABLE_PUSH_CREATE_USER`: **false**: Allow users to push local repositories to Gitea and have them automatically created for a user.
70+
- `ENABLE_PUSH_CREATE_ORG`: **false**: Allow users to push local repositories to Gitea and have them automatically created for an org.
6971

7072
### Repository - Pull Request (`repository.pull-request`)
7173

integrations/git_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func testGit(t *testing.T, u *url.URL) {
7575
rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
7676
mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
7777
})
78+
79+
t.Run("PushCreate", doPushCreate(httpContext, u))
7880
})
7981
t.Run("SSH", func(t *testing.T) {
8082
defer PrintCurrentTest(t)()
@@ -113,6 +115,8 @@ func testGit(t *testing.T, u *url.URL) {
113115
rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
114116
mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
115117
})
118+
119+
t.Run("PushCreate", doPushCreate(sshContext, sshURL))
116120
})
117121
})
118122
}
@@ -408,3 +412,57 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
408412

409413
}
410414
}
415+
416+
func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
417+
return func(t *testing.T) {
418+
defer PrintCurrentTest(t)()
419+
ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
420+
u.Path = ctx.GitPath()
421+
422+
tmpDir, err := ioutil.TempDir("", ctx.Reponame)
423+
assert.NoError(t, err)
424+
425+
err = git.InitRepository(tmpDir, false)
426+
assert.NoError(t, err)
427+
428+
_, err = os.Create(filepath.Join(tmpDir, "test.txt"))
429+
assert.NoError(t, err)
430+
431+
err = git.AddChanges(tmpDir, true)
432+
assert.NoError(t, err)
433+
434+
err = git.CommitChanges(tmpDir, git.CommitChangesOptions{
435+
Committer: &git.Signature{
436+
437+
Name: "User Two",
438+
When: time.Now(),
439+
},
440+
Author: &git.Signature{
441+
442+
Name: "User Two",
443+
When: time.Now(),
444+
},
445+
Message: fmt.Sprintf("Testing push create @ %v", time.Now()),
446+
})
447+
assert.NoError(t, err)
448+
449+
_, err = git.NewCommand("remote", "add", "origin", u.String()).RunInDir(tmpDir)
450+
assert.NoError(t, err)
451+
452+
// Push to create disabled
453+
setting.Repository.EnablePushCreateUser = false
454+
_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
455+
assert.Error(t, err)
456+
457+
// Push to create enabled
458+
setting.Repository.EnablePushCreateUser = true
459+
_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
460+
assert.NoError(t, err)
461+
462+
// Fetch repo from database
463+
repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
464+
assert.NoError(t, err)
465+
assert.False(t, repo.IsEmpty)
466+
assert.True(t, repo.IsPrivate)
467+
}
468+
}

modules/setting/repository.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ var (
3535
AccessControlAllowOrigin string
3636
UseCompatSSHURI bool
3737
DefaultCloseIssuesViaCommitsInAnyBranch bool
38+
EnablePushCreateUser bool
39+
EnablePushCreateOrg bool
3840

3941
// Repository editor settings
4042
Editor struct {
@@ -89,6 +91,8 @@ var (
8991
AccessControlAllowOrigin: "",
9092
UseCompatSSHURI: false,
9193
DefaultCloseIssuesViaCommitsInAnyBranch: false,
94+
EnablePushCreateUser: false,
95+
EnablePushCreateOrg: false,
9296

9397
// Repository editor settings
9498
Editor: struct {

routers/private/serv.go

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/private"
1616
"code.gitea.io/gitea/modules/setting"
17+
repo_service "code.gitea.io/gitea/services/repository"
1718

1819
"gitea.com/macaron/macaron"
1920
)
@@ -98,44 +99,44 @@ func ServCommand(ctx *macaron.Context) {
9899
}
99100

100101
// Now get the Repository and set the results section
102+
repoExist := true
101103
repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName)
102104
if err != nil {
103105
if models.IsErrRepoNotExist(err) {
104-
ctx.JSON(http.StatusNotFound, map[string]interface{}{
106+
repoExist = false
107+
} else {
108+
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
109+
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
105110
"results": results,
106-
"type": "ErrRepoNotExist",
107-
"err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
111+
"type": "InternalServerError",
112+
"err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
108113
})
109114
return
110115
}
111-
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
112-
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
113-
"results": results,
114-
"type": "InternalServerError",
115-
"err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
116-
})
117-
return
118116
}
119-
repo.OwnerName = ownerName
120-
results.RepoID = repo.ID
121117

122-
if repo.IsBeingCreated() {
123-
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
124-
"results": results,
125-
"type": "InternalServerError",
126-
"err": "Repository is being created, you could retry after it finished",
127-
})
128-
return
129-
}
118+
if repoExist {
119+
repo.OwnerName = ownerName
120+
results.RepoID = repo.ID
130121

131-
// We can shortcut at this point if the repo is a mirror
132-
if mode > models.AccessModeRead && repo.IsMirror {
133-
ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
134-
"results": results,
135-
"type": "ErrMirrorReadOnly",
136-
"err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
137-
})
138-
return
122+
if repo.IsBeingCreated() {
123+
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
124+
"results": results,
125+
"type": "InternalServerError",
126+
"err": "Repository is being created, you could retry after it finished",
127+
})
128+
return
129+
}
130+
131+
// We can shortcut at this point if the repo is a mirror
132+
if mode > models.AccessModeRead && repo.IsMirror {
133+
ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
134+
"results": results,
135+
"type": "ErrMirrorReadOnly",
136+
"err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
137+
})
138+
return
139+
}
139140
}
140141

141142
// Get the Public Key represented by the keyID
@@ -161,6 +162,16 @@ func ServCommand(ctx *macaron.Context) {
161162
results.KeyID = key.ID
162163
results.UserID = key.OwnerID
163164

165+
// If repo doesn't exist, deploy key doesn't make sense
166+
if !repoExist && key.Type == models.KeyTypeDeploy {
167+
ctx.JSON(http.StatusNotFound, map[string]interface{}{
168+
"results": results,
169+
"type": "ErrRepoNotExist",
170+
"err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
171+
})
172+
return
173+
}
174+
164175
// Deploy Keys have ownerID set to 0 therefore we can't use the owner
165176
// So now we need to check if the key is a deploy key
166177
// We'll keep hold of the deploy key here for permissions checking
@@ -220,7 +231,7 @@ func ServCommand(ctx *macaron.Context) {
220231
}
221232

222233
// Don't allow pushing if the repo is archived
223-
if mode > models.AccessModeRead && repo.IsArchived {
234+
if repoExist && mode > models.AccessModeRead && repo.IsArchived {
224235
ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
225236
"results": results,
226237
"type": "ErrRepoIsArchived",
@@ -230,7 +241,7 @@ func ServCommand(ctx *macaron.Context) {
230241
}
231242

232243
// Permissions checking:
233-
if mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView {
244+
if repoExist && (mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView) {
234245
if key.Type == models.KeyTypeDeploy {
235246
if deployKey.Mode < mode {
236247
ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
@@ -265,6 +276,48 @@ func ServCommand(ctx *macaron.Context) {
265276
}
266277
}
267278

279+
// We already know we aren't using a deploy key
280+
if !repoExist {
281+
owner, err := models.GetUserByName(ownerName)
282+
if err != nil {
283+
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
284+
"results": results,
285+
"type": "InternalServerError",
286+
"err": fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err),
287+
})
288+
return
289+
}
290+
291+
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
292+
ctx.JSON(http.StatusForbidden, map[string]interface{}{
293+
"results": results,
294+
"type": "ErrForbidden",
295+
"err": "Push to create is not enabled for organizations.",
296+
})
297+
return
298+
}
299+
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
300+
ctx.JSON(http.StatusForbidden, map[string]interface{}{
301+
"results": results,
302+
"type": "ErrForbidden",
303+
"err": "Push to create is not enabled for users.",
304+
})
305+
return
306+
}
307+
308+
repo, err = repo_service.PushCreateRepo(user, owner, results.RepoName)
309+
if err != nil {
310+
log.Error("pushCreateRepo: %v", err)
311+
ctx.JSON(http.StatusNotFound, map[string]interface{}{
312+
"results": results,
313+
"type": "ErrRepoNotExist",
314+
"err": fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
315+
})
316+
return
317+
}
318+
results.RepoID = repo.ID
319+
}
320+
268321
// Finally if we're trying to touch the wiki we should init it
269322
if results.IsWiki {
270323
if err = repo.InitWiki(); err != nil {

routers/repo/http.go

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"code.gitea.io/gitea/modules/process"
2929
"code.gitea.io/gitea/modules/setting"
3030
"code.gitea.io/gitea/modules/timeutil"
31+
repo_service "code.gitea.io/gitea/services/repository"
3132
)
3233

3334
// HTTP implmentation git smart HTTP protocol
@@ -100,29 +101,29 @@ func HTTP(ctx *context.Context) {
100101
return
101102
}
102103

104+
repoExist := true
103105
repo, err := models.GetRepositoryByName(owner.ID, reponame)
104106
if err != nil {
105107
if models.IsErrRepoNotExist(err) {
106-
redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame)
107-
if err == nil {
108+
if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
108109
context.RedirectToRepo(ctx, redirectRepoID)
109-
} else {
110-
ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err)
110+
return
111111
}
112+
repoExist = false
112113
} else {
113114
ctx.ServerError("GetRepositoryByName", err)
115+
return
114116
}
115-
return
116117
}
117118

118119
// Don't allow pushing if the repo is archived
119-
if repo.IsArchived && !isPull {
120+
if repoExist && repo.IsArchived && !isPull {
120121
ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
121122
return
122123
}
123124

124125
// Only public pull don't need auth.
125-
isPublicPull := !repo.IsPrivate && isPull
126+
isPublicPull := repoExist && !repo.IsPrivate && isPull
126127
var (
127128
askAuth = !isPublicPull || setting.Service.RequireSignInView
128129
authUser *models.User
@@ -243,28 +244,29 @@ func HTTP(ctx *context.Context) {
243244
}
244245
}
245246

246-
perm, err := models.GetUserRepoPermission(repo, authUser)
247-
if err != nil {
248-
ctx.ServerError("GetUserRepoPermission", err)
249-
return
250-
}
247+
if repoExist {
248+
perm, err := models.GetUserRepoPermission(repo, authUser)
249+
if err != nil {
250+
ctx.ServerError("GetUserRepoPermission", err)
251+
return
252+
}
251253

252-
if !perm.CanAccess(accessMode, unitType) {
253-
ctx.HandleText(http.StatusForbidden, "User permission denied")
254-
return
255-
}
254+
if !perm.CanAccess(accessMode, unitType) {
255+
ctx.HandleText(http.StatusForbidden, "User permission denied")
256+
return
257+
}
256258

257-
if !isPull && repo.IsMirror {
258-
ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
259-
return
259+
if !isPull && repo.IsMirror {
260+
ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
261+
return
262+
}
260263
}
261264

262265
environ = []string{
263266
models.EnvRepoUsername + "=" + username,
264267
models.EnvRepoName + "=" + reponame,
265268
models.EnvPusherName + "=" + authUser.Name,
266269
models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
267-
models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID),
268270
models.EnvIsDeployKey + "=false",
269271
}
270272

@@ -279,6 +281,25 @@ func HTTP(ctx *context.Context) {
279281
}
280282
}
281283

284+
if !repoExist {
285+
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
286+
ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
287+
return
288+
}
289+
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
290+
ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
291+
return
292+
}
293+
repo, err = repo_service.PushCreateRepo(authUser, owner, reponame)
294+
if err != nil {
295+
log.Error("pushCreateRepo: %v", err)
296+
ctx.Status(http.StatusNotFound)
297+
return
298+
}
299+
}
300+
301+
environ = append(environ, models.ProtectedBranchRepoID+fmt.Sprintf("=%d", repo.ID))
302+
282303
w := ctx.Resp
283304
r := ctx.Req.Request
284305
cfg := &serviceConfig{

0 commit comments

Comments
 (0)