diff --git a/cmd/serv.go b/cmd/serv.go index 21a69b24d..ca0354d06 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -234,19 +234,20 @@ func runServ(c *cli.Context) error { // Check deploy key or user key. if key.Type == models.KeyTypeDeploy { - if key.Mode < requestedMode { - fail("Key permission denied", "Cannot push with deployment key: %d", key.ID) - } - - // Check if this deploy key belongs to current repository. - has, err := private.HasDeployKey(key.ID, repo.ID) + // Now we have to get the deploy key for this repo + deployKey, err := private.GetDeployKey(key.ID, repo.ID) if err != nil { fail("Key access denied", "Failed to access internal api: [key_id: %d, repo_id: %d]", key.ID, repo.ID) } - if !has { + + if deployKey == nil { fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID) } + if deployKey.Mode < requestedMode { + fail("Key permission denied", "Cannot push with read-only deployment key: %d to repo_id: %d", key.ID, repo.ID) + } + // Update deploy key activity. if err = private.UpdateDeployKeyUpdated(key.ID, repo.ID); err != nil { fail("Internal error", "UpdateDeployKey: %v", err) diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go new file mode 100644 index 000000000..32a4ce804 --- /dev/null +++ b/integrations/api_helper_for_declarative_test.go @@ -0,0 +1,152 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + + api "code.gitea.io/sdk/gitea" + "github.com/stretchr/testify/assert" +) + +type APITestContext struct { + Reponame string + Session *TestSession + Token string + Username string + ExpectedCode int +} + +func NewAPITestContext(t *testing.T, username, reponame string) APITestContext { + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session) + return APITestContext{ + Session: session, + Token: token, + Username: username, + Reponame: reponame, + } +} + +func (ctx APITestContext) GitPath() string { + return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame) +} + +func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) { + return func(t *testing.T) { + createRepoOption := &api.CreateRepoOption{ + AutoInit: !empty, + Description: "Temporary repo", + Name: ctx.Reponame, + Private: true, + Gitignores: "", + License: "WTFPL", + Readme: "Default", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+ctx.Token, createRepoOption) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + resp := ctx.Session.MakeRequest(t, req, http.StatusCreated) + + var repository api.Repository + DecodeJSON(t, resp, &repository) + if len(callback) > 0 { + callback[0](t, repository) + } + } +} + +func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token) + + req := NewRequest(t, "GET", urlStr) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var repository api.Repository + DecodeJSON(t, resp, &repository) + if len(callback) > 0 { + callback[0](t, repository) + } + } +} + +func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token) + + req := NewRequest(t, "DELETE", urlStr) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, http.StatusNoContent) + } +} + +func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/user/keys?token=%s", ctx.Token) + + dataPubKey, err := ioutil.ReadFile(keyFile + ".pub") + assert.NoError(t, err) + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateKeyOption{ + Title: keyname, + Key: string(dataPubKey), + }) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + resp := ctx.Session.MakeRequest(t, req, http.StatusCreated) + var publicKey api.PublicKey + DecodeJSON(t, resp, &publicKey) + if len(callback) > 0 { + callback[0](t, publicKey) + } + } +} + +func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/user/keys/%d?token=%s", keyID, ctx.Token) + + req := NewRequest(t, "DELETE", urlStr) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, http.StatusNoContent) + } +} + +func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", ctx.Username, ctx.Reponame, ctx.Token) + + dataPubKey, err := ioutil.ReadFile(keyFile + ".pub") + assert.NoError(t, err) + req := NewRequestWithJSON(t, "POST", urlStr, api.CreateKeyOption{ + Title: keyname, + Key: string(dataPubKey), + ReadOnly: readOnly, + }) + + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, http.StatusCreated) + } +} diff --git a/integrations/deploy_key_push_test.go b/integrations/deploy_key_push_test.go deleted file mode 100644 index 8b3d66562..000000000 --- a/integrations/deploy_key_push_test.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package integrations - -import ( - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - "code.gitea.io/git" - - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/sdk/gitea" - "github.com/stretchr/testify/assert" -) - -func createEmptyRepository(username, reponame string) func(*testing.T) { - return func(t *testing.T) { - session := loginUser(t, username) - token := getTokenForLoggedInUser(t, session) - req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+token, &api.CreateRepoOption{ - AutoInit: false, - Description: "Temporary empty repo", - Name: reponame, - Private: false, - }) - session.MakeRequest(t, req, http.StatusCreated) - } -} - -func createDeployKey(username, reponame, keyname, keyFile string, readOnly bool) func(*testing.T) { - return func(t *testing.T) { - session := loginUser(t, username) - token := getTokenForLoggedInUser(t, session) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", username, reponame, token) - - dataPubKey, err := ioutil.ReadFile(keyFile + ".pub") - assert.NoError(t, err) - req := NewRequestWithJSON(t, "POST", urlStr, api.CreateKeyOption{ - Title: keyname, - Key: string(dataPubKey), - ReadOnly: readOnly, - }) - session.MakeRequest(t, req, http.StatusCreated) - } -} - -func initTestRepository(dstPath string) func(*testing.T) { - return func(t *testing.T) { - // Init repository in dstPath - assert.NoError(t, git.InitRepository(dstPath, false)) - assert.NoError(t, ioutil.WriteFile(filepath.Join(dstPath, "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", dstPath)), 0644)) - assert.NoError(t, git.AddChanges(dstPath, true)) - signature := git.Signature{ - Email: "test@example.com", - Name: "test", - When: time.Now(), - } - assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{ - Committer: &signature, - Author: &signature, - Message: "Initial Commit", - })) - } -} - -func pushTestRepository(dstPath, username, reponame string, u url.URL, keyFile string) func(*testing.T) { - return func(t *testing.T) { - //Setup remote link - u.Scheme = "ssh" - u.User = url.User("git") - u.Host = fmt.Sprintf("%s:%d", setting.SSH.ListenHost, setting.SSH.ListenPort) - - //Setup ssh wrapper - os.Setenv("GIT_SSH_COMMAND", - "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i "+ - filepath.Join(setting.AppWorkPath, keyFile)) - os.Setenv("GIT_SSH_VARIANT", "ssh") - - log.Printf("Adding remote: %s\n", u.String()) - _, err := git.NewCommand("remote", "add", "origin", u.String()).RunInDir(dstPath) - assert.NoError(t, err) - - log.Printf("Pushing to: %s\n", u.String()) - _, err = git.NewCommand("push", "-u", "origin", "master").RunInDir(dstPath) - assert.NoError(t, err) - } -} - -func checkRepositoryEmptyStatus(username, reponame string, isEmpty bool) func(*testing.T) { - return func(t *testing.T) { - session := loginUser(t, username) - token := getTokenForLoggedInUser(t, session) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", username, reponame, token) - - req := NewRequest(t, "GET", urlStr) - resp := session.MakeRequest(t, req, http.StatusOK) - - var repository api.Repository - DecodeJSON(t, resp, &repository) - - assert.Equal(t, isEmpty, repository.Empty) - } -} - -func deleteRepository(username, reponame string) func(*testing.T) { - return func(t *testing.T) { - session := loginUser(t, username) - token := getTokenForLoggedInUser(t, session) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", username, reponame, token) - - req := NewRequest(t, "DELETE", urlStr) - session.MakeRequest(t, req, http.StatusNoContent) - } -} - -func TestPushDeployKeyOnEmptyRepo(t *testing.T) { - onGiteaRun(t, testPushDeployKeyOnEmptyRepo) -} - -func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) { - reponame := "deploy-key-empty-repo-1" - username := "user2" - u.Path = fmt.Sprintf("%s/%s.git", username, reponame) - keyname := fmt.Sprintf("%s-push", reponame) - - t.Run("CreateEmptyRepository", createEmptyRepository(username, reponame)) - t.Run("CheckIsEmpty", checkRepositoryEmptyStatus(username, reponame, true)) - - //Setup the push deploy key file - keyFile := filepath.Join(setting.AppDataPath, keyname) - err := exec.Command("ssh-keygen", "-f", keyFile, "-t", "rsa", "-N", "").Run() - assert.NoError(t, err) - defer os.RemoveAll(keyFile) - defer os.RemoveAll(keyFile + ".pub") - - t.Run("CreatePushDeployKey", createDeployKey(username, reponame, keyname, keyFile, false)) - - // Setup the testing repository - dstPath, err := ioutil.TempDir("", "repo-tmp-deploy-key-empty-repo-1") - assert.NoError(t, err) - defer os.RemoveAll(dstPath) - - t.Run("InitTestRepository", initTestRepository(dstPath)) - t.Run("SSHPushTestRepository", pushTestRepository(dstPath, username, reponame, *u, keyFile)) - - log.Println("Done Push") - t.Run("CheckIsNotEmpty", checkRepositoryEmptyStatus(username, reponame, false)) - - t.Run("DeleteRepository", deleteRepository(username, reponame)) -} diff --git a/integrations/git_helper_for_declarative_test.go b/integrations/git_helper_for_declarative_test.go new file mode 100644 index 000000000..572abe95a --- /dev/null +++ b/integrations/git_helper_for_declarative_test.go @@ -0,0 +1,127 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "context" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "code.gitea.io/git" + "code.gitea.io/gitea/modules/setting" + "github.com/Unknwon/com" + "github.com/stretchr/testify/assert" +) + +func withKeyFile(t *testing.T, keyname string, callback func(string)) { + keyFile := filepath.Join(setting.AppDataPath, keyname) + err := exec.Command("ssh-keygen", "-f", keyFile, "-t", "rsa", "-N", "").Run() + assert.NoError(t, err) + + //Setup ssh wrapper + os.Setenv("GIT_SSH_COMMAND", + "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i "+ + filepath.Join(setting.AppWorkPath, keyFile)) + os.Setenv("GIT_SSH_VARIANT", "ssh") + + callback(keyFile) + + defer os.RemoveAll(keyFile) + defer os.RemoveAll(keyFile + ".pub") +} + +func createSSHUrl(gitPath string, u *url.URL) *url.URL { + u2 := *u + u2.Scheme = "ssh" + u2.User = url.User("git") + u2.Host = fmt.Sprintf("%s:%d", setting.SSH.ListenHost, setting.SSH.ListenPort) + u2.Path = gitPath + return &u2 +} + +func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) { + prepareTestEnv(t) + s := http.Server{ + Handler: mac, + } + + u, err := url.Parse(setting.AppURL) + assert.NoError(t, err) + listener, err := net.Listen("tcp", u.Host) + assert.NoError(t, err) + + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + s.Shutdown(ctx) + cancel() + }() + + go s.Serve(listener) + //Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) + + callback(t, u) +} + +func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { + return func(t *testing.T) { + assert.NoError(t, git.Clone(u.String(), dstLocalPath, git.CloneRepoOptions{})) + assert.True(t, com.IsExist(filepath.Join(dstLocalPath, "README.md"))) + } +} + +func doGitCloneFail(dstLocalPath string, u *url.URL) func(*testing.T) { + return func(t *testing.T) { + assert.Error(t, git.Clone(u.String(), dstLocalPath, git.CloneRepoOptions{})) + assert.False(t, com.IsExist(filepath.Join(dstLocalPath, "README.md"))) + } +} + +func doGitInitTestRepository(dstPath string) func(*testing.T) { + return func(t *testing.T) { + // Init repository in dstPath + assert.NoError(t, git.InitRepository(dstPath, false)) + assert.NoError(t, ioutil.WriteFile(filepath.Join(dstPath, "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", dstPath)), 0644)) + assert.NoError(t, git.AddChanges(dstPath, true)) + signature := git.Signature{ + Email: "test@example.com", + Name: "test", + When: time.Now(), + } + assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{ + Committer: &signature, + Author: &signature, + Message: "Initial Commit", + })) + } +} + +func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand("remote", "add", remoteName, u.String()).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitPushTestRepository(dstPath, remoteName, branch string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitPushTestRepositoryFail(dstPath, remoteName, branch string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + assert.Error(t, err) + } +} diff --git a/integrations/git_test.go b/integrations/git_test.go index 96d39e051..a37252586 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -5,25 +5,17 @@ package integrations import ( - "context" "crypto/rand" "fmt" "io/ioutil" - "net" - "net/http" "net/url" "os" - "os/exec" "path/filepath" "testing" "time" "code.gitea.io/git" - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/sdk/gitea" - "github.com/Unknwon/com" "github.com/stretchr/testify/assert" ) @@ -32,160 +24,86 @@ const ( bigSize = 128 * 1024 * 1024 //128Mo ) -func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) { - prepareTestEnv(t) - s := http.Server{ - Handler: mac, - } +func TestGit(t *testing.T) { + onGiteaRun(t, testGit) +} - u, err := url.Parse(setting.AppURL) - assert.NoError(t, err) - listener, err := net.Listen("tcp", u.Host) - assert.NoError(t, err) +func testGit(t *testing.T, u *url.URL) { + username := "user2" + baseAPITestContext := NewAPITestContext(t, username, "repo1") - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - s.Shutdown(ctx) - cancel() - }() + u.Path = baseAPITestContext.GitPath() - go s.Serve(listener) - //Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) + t.Run("HTTP", func(t *testing.T) { + httpContext := baseAPITestContext + httpContext.Reponame = "repo-tmp-17" - callback(t, u) -} + dstPath, err := ioutil.TempDir("", httpContext.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + t.Run("Standard", func(t *testing.T) { + ensureAnonymousClone(t, u) -func TestGit(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - u.Path = "user2/repo1.git" + t.Run("CreateRepo", doAPICreateRepository(httpContext, false)) - t.Run("HTTP", func(t *testing.T) { - dstPath, err := ioutil.TempDir("", "repo-tmp-17") - assert.NoError(t, err) - defer os.RemoveAll(dstPath) - t.Run("Standard", func(t *testing.T) { - t.Run("CloneNoLogin", func(t *testing.T) { - dstLocalPath, err := ioutil.TempDir("", "repo1") - assert.NoError(t, err) - defer os.RemoveAll(dstLocalPath) - err = git.Clone(u.String(), dstLocalPath, git.CloneRepoOptions{}) - assert.NoError(t, err) - assert.True(t, com.IsExist(filepath.Join(dstLocalPath, "README.md"))) - }) + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) - t.Run("CreateRepo", func(t *testing.T) { - session := loginUser(t, "user2") - token := getTokenForLoggedInUser(t, session) - req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+token, &api.CreateRepoOption{ - AutoInit: true, - Description: "Temporary repo", - Name: "repo-tmp-17", - Private: false, - Gitignores: "", - License: "WTFPL", - Readme: "Default", - }) - session.MakeRequest(t, req, http.StatusCreated) - }) + t.Run("Clone", doGitClone(dstPath, u)) - u.Path = "user2/repo-tmp-17.git" - u.User = url.UserPassword("user2", userPassword) - t.Run("Clone", func(t *testing.T) { - err = git.Clone(u.String(), dstPath, git.CloneRepoOptions{}) - assert.NoError(t, err) - assert.True(t, com.IsExist(filepath.Join(dstPath, "README.md"))) + t.Run("PushCommit", func(t *testing.T) { + t.Run("Little", func(t *testing.T) { + commitAndPush(t, littleSize, dstPath) }) - - t.Run("PushCommit", func(t *testing.T) { - t.Run("Little", func(t *testing.T) { - commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - commitAndPush(t, bigSize, dstPath) - }) + t.Run("Big", func(t *testing.T) { + commitAndPush(t, bigSize, dstPath) }) }) - t.Run("LFS", func(t *testing.T) { - t.Run("PushCommit", func(t *testing.T) { - //Setup git LFS - _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - t.Run("Little", func(t *testing.T) { - commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - commitAndPush(t, bigSize, dstPath) - }) + }) + t.Run("LFS", func(t *testing.T) { + t.Run("PushCommit", func(t *testing.T) { + //Setup git LFS + _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) + assert.NoError(t, err) + _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) + assert.NoError(t, err) + err = git.AddChanges(dstPath, false, ".gitattributes") + assert.NoError(t, err) + + t.Run("Little", func(t *testing.T) { + commitAndPush(t, littleSize, dstPath) }) - t.Run("Locks", func(t *testing.T) { - lockTest(t, u.String(), dstPath) + t.Run("Big", func(t *testing.T) { + commitAndPush(t, bigSize, dstPath) }) }) - }) - t.Run("SSH", func(t *testing.T) { - //Setup remote link - u.Scheme = "ssh" - u.User = url.User("git") - u.Host = fmt.Sprintf("%s:%d", setting.SSH.ListenHost, setting.SSH.ListenPort) - u.Path = "user2/repo-tmp-18.git" - - //Setup key - keyFile := filepath.Join(setting.AppDataPath, "my-testing-key") - err := exec.Command("ssh-keygen", "-f", keyFile, "-t", "rsa", "-N", "").Run() - assert.NoError(t, err) - defer os.RemoveAll(keyFile) - defer os.RemoveAll(keyFile + ".pub") - - session := loginUser(t, "user1") - keyOwner := models.AssertExistsAndLoadBean(t, &models.User{Name: "user2"}).(*models.User) - token := getTokenForLoggedInUser(t, session) - urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", keyOwner.Name, token) - - dataPubKey, err := ioutil.ReadFile(keyFile + ".pub") - assert.NoError(t, err) - req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ - "key": string(dataPubKey), - "title": "test-key", + t.Run("Locks", func(t *testing.T) { + lockTest(t, u.String(), dstPath) }) - session.MakeRequest(t, req, http.StatusCreated) + }) + }) + t.Run("SSH", func(t *testing.T) { + sshContext := baseAPITestContext + sshContext.Reponame = "repo-tmp-18" + keyname := "my-testing-key" + //Setup key the user ssh key + withKeyFile(t, keyname, func(keyFile string) { + t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) - //Setup ssh wrapper - os.Setenv("GIT_SSH_COMMAND", - "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i "+ - filepath.Join(setting.AppWorkPath, keyFile)) - os.Setenv("GIT_SSH_VARIANT", "ssh") + //Setup remote link + sshURL := createSSHUrl(sshContext.GitPath(), u) //Setup clone folder - dstPath, err := ioutil.TempDir("", "repo-tmp-18") + dstPath, err := ioutil.TempDir("", sshContext.Reponame) assert.NoError(t, err) defer os.RemoveAll(dstPath) t.Run("Standard", func(t *testing.T) { - t.Run("CreateRepo", func(t *testing.T) { - session := loginUser(t, "user2") - token := getTokenForLoggedInUser(t, session) - req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+token, &api.CreateRepoOption{ - AutoInit: true, - Description: "Temporary repo", - Name: "repo-tmp-18", - Private: false, - Gitignores: "", - License: "WTFPL", - Readme: "Default", - }) - session.MakeRequest(t, req, http.StatusCreated) - }) + t.Run("CreateRepo", doAPICreateRepository(sshContext, false)) + //TODO get url from api - t.Run("Clone", func(t *testing.T) { - _, err = git.NewCommand("clone").AddArguments(u.String(), dstPath).Run() - assert.NoError(t, err) - assert.True(t, com.IsExist(filepath.Join(dstPath, "README.md"))) - }) + t.Run("Clone", doGitClone(dstPath, sshURL)) + //time.Sleep(5 * time.Minute) t.Run("PushCommit", func(t *testing.T) { t.Run("Little", func(t *testing.T) { @@ -217,10 +135,20 @@ func TestGit(t *testing.T) { lockTest(t, u.String(), dstPath) }) }) + }) + }) } +func ensureAnonymousClone(t *testing.T, u *url.URL) { + dstLocalPath, err := ioutil.TempDir("", "repo1") + assert.NoError(t, err) + defer os.RemoveAll(dstLocalPath) + t.Run("CloneAnonymous", doGitClone(dstLocalPath, u)) + +} + func lockTest(t *testing.T, remote, repoPath string) { _, err := git.NewCommand("remote").AddArguments("set-url", "origin", remote).RunInDir(repoPath) //TODO add test ssh git-lfs-creds assert.NoError(t, err) diff --git a/integrations/ssh_key_test.go b/integrations/ssh_key_test.go new file mode 100644 index 000000000..9ad43ae22 --- /dev/null +++ b/integrations/ssh_key_test.go @@ -0,0 +1,217 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "code.gitea.io/git" + api "code.gitea.io/sdk/gitea" + "github.com/stretchr/testify/assert" +) + +func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testing.T) { + return doAPIGetRepository(ctx, func(t *testing.T, repository api.Repository) { + assert.Equal(t, isEmpty, repository.Empty) + }) +} + +func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) { + return func(t *testing.T) { + assert.NoError(t, ioutil.WriteFile(filepath.Join(dstPath, filename), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s at time: %v", dstPath, time.Now())), 0644)) + assert.NoError(t, git.AddChanges(dstPath, true)) + signature := git.Signature{ + Email: "test@example.com", + Name: "test", + When: time.Now(), + } + assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{ + Committer: &signature, + Author: &signature, + Message: "Initial Commit", + })) + } +} + +func TestPushDeployKeyOnEmptyRepo(t *testing.T) { + onGiteaRun(t, testPushDeployKeyOnEmptyRepo) +} + +func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) { + // OK login + ctx := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1") + keyname := fmt.Sprintf("%s-push", ctx.Reponame) + u.Path = ctx.GitPath() + + t.Run("CreateEmptyRepository", doAPICreateRepository(ctx, true)) + + t.Run("CheckIsEmpty", doCheckRepositoryEmptyStatus(ctx, true)) + + withKeyFile(t, keyname, func(keyFile string) { + t.Run("CreatePushDeployKey", doAPICreateDeployKey(ctx, keyname, keyFile, false)) + + // Setup the testing repository + dstPath, err := ioutil.TempDir("", "repo-tmp-deploy-key-empty-repo-1") + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + + t.Run("InitTestRepository", doGitInitTestRepository(dstPath)) + + //Setup remote link + sshURL := createSSHUrl(ctx.GitPath(), u) + + t.Run("AddRemote", doGitAddRemote(dstPath, "origin", sshURL)) + + t.Run("SSHPushTestRepository", doGitPushTestRepository(dstPath, "origin", "master")) + + t.Run("CheckIsNotEmpty", doCheckRepositoryEmptyStatus(ctx, false)) + + t.Run("DeleteRepository", doAPIDeleteRepository(ctx)) + }) +} + +func TestKeyOnlyOneType(t *testing.T) { + onGiteaRun(t, testKeyOnlyOneType) +} + +func testKeyOnlyOneType(t *testing.T, u *url.URL) { + // Once a key is a user key we cannot use it as a deploy key + // If we delete it from the user we should be able to use it as a deploy key + reponame := "ssh-key-test-repo" + username := "user2" + u.Path = fmt.Sprintf("%s/%s.git", username, reponame) + keyname := fmt.Sprintf("%s-push", reponame) + + // OK login + ctx := NewAPITestContext(t, username, reponame) + + otherCtx := ctx + otherCtx.Reponame = "ssh-key-test-repo-2" + + failCtx := ctx + failCtx.ExpectedCode = http.StatusUnprocessableEntity + + t.Run("CreateRepository", doAPICreateRepository(ctx, false)) + t.Run("CreateOtherRepository", doAPICreateRepository(otherCtx, false)) + + withKeyFile(t, keyname, func(keyFile string) { + var userKeyPublicKeyID int64 + t.Run("KeyCanOnlyBeUser", func(t *testing.T) { + dstPath, err := ioutil.TempDir("", ctx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + + sshURL := createSSHUrl(ctx.GitPath(), u) + + t.Run("FailToClone", doGitCloneFail(dstPath, sshURL)) + + t.Run("CreateUserKey", doAPICreateUserKey(ctx, keyname, keyFile, func(t *testing.T, publicKey api.PublicKey) { + userKeyPublicKeyID = publicKey.ID + })) + + t.Run("FailToAddReadOnlyDeployKey", doAPICreateDeployKey(failCtx, keyname, keyFile, true)) + + t.Run("FailToAddDeployKey", doAPICreateDeployKey(failCtx, keyname, keyFile, false)) + + t.Run("Clone", doGitClone(dstPath, sshURL)) + + t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES1.md")) + + t.Run("Push", doGitPushTestRepository(dstPath, "origin", "master")) + + t.Run("DeleteUserKey", doAPIDeleteUserKey(ctx, userKeyPublicKeyID)) + }) + + t.Run("KeyCanBeAnyDeployButNotUserAswell", func(t *testing.T) { + dstPath, err := ioutil.TempDir("", ctx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + + sshURL := createSSHUrl(ctx.GitPath(), u) + + t.Run("FailToClone", doGitCloneFail(dstPath, sshURL)) + + // Should now be able to add... + t.Run("AddReadOnlyDeployKey", doAPICreateDeployKey(ctx, keyname, keyFile, true)) + + t.Run("Clone", doGitClone(dstPath, sshURL)) + + t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES2.md")) + + t.Run("FailToPush", doGitPushTestRepositoryFail(dstPath, "origin", "master")) + + otherSSHURL := createSSHUrl(otherCtx.GitPath(), u) + dstOtherPath, err := ioutil.TempDir("", otherCtx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstOtherPath) + + t.Run("AddWriterDeployKeyToOther", doAPICreateDeployKey(otherCtx, keyname, keyFile, false)) + + t.Run("CloneOther", doGitClone(dstOtherPath, otherSSHURL)) + + t.Run("AddChangesToOther", doAddChangesToCheckout(dstOtherPath, "CHANGES3.md")) + + t.Run("PushToOther", doGitPushTestRepository(dstOtherPath, "origin", "master")) + + t.Run("FailToCreateUserKey", doAPICreateUserKey(failCtx, keyname, keyFile)) + }) + + t.Run("DeleteRepositoryShouldReleaseKey", func(t *testing.T) { + otherSSHURL := createSSHUrl(otherCtx.GitPath(), u) + dstOtherPath, err := ioutil.TempDir("", otherCtx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstOtherPath) + + t.Run("DeleteRepository", doAPIDeleteRepository(ctx)) + + t.Run("FailToCreateUserKeyAsStillDeploy", doAPICreateUserKey(failCtx, keyname, keyFile)) + + t.Run("MakeSureCloneOtherStillWorks", doGitClone(dstOtherPath, otherSSHURL)) + + t.Run("AddChangesToOther", doAddChangesToCheckout(dstOtherPath, "CHANGES3.md")) + + t.Run("PushToOther", doGitPushTestRepository(dstOtherPath, "origin", "master")) + + t.Run("DeleteOtherRepository", doAPIDeleteRepository(otherCtx)) + + t.Run("RecreateRepository", doAPICreateRepository(ctx, false)) + + t.Run("CreateUserKey", doAPICreateUserKey(ctx, keyname, keyFile, func(t *testing.T, publicKey api.PublicKey) { + userKeyPublicKeyID = publicKey.ID + })) + + dstPath, err := ioutil.TempDir("", ctx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + + sshURL := createSSHUrl(ctx.GitPath(), u) + + t.Run("Clone", doGitClone(dstPath, sshURL)) + + t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES1.md")) + + t.Run("Push", doGitPushTestRepository(dstPath, "origin", "master")) + }) + + t.Run("DeleteUserKeyShouldRemoveAbilityToClone", func(t *testing.T) { + dstPath, err := ioutil.TempDir("", ctx.Reponame) + assert.NoError(t, err) + defer os.RemoveAll(dstPath) + + sshURL := createSSHUrl(ctx.GitPath(), u) + + t.Run("DeleteUserKey", doAPIDeleteUserKey(ctx, userKeyPublicKeyID)) + + t.Run("FailToClone", doGitCloneFail(dstPath, sshURL)) + }) + }) +} diff --git a/models/repo.go b/models/repo.go index 5e96a4e93..873fd407f 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1756,6 +1756,17 @@ func DeleteRepository(doer *User, uid, repoID int64) error { return ErrRepoNotExist{repoID, uid, "", ""} } + // Delete Deploy Keys + deployKeys, err := listDeployKeys(sess, repo.ID) + if err != nil { + return fmt.Errorf("listDeployKeys: %v", err) + } + for _, dKey := range deployKeys { + if err := deleteDeployKey(sess, doer, dKey.ID); err != nil { + return fmt.Errorf("deleteDeployKeys: %v", err) + } + } + if cnt, err := sess.ID(repoID).Delete(&Repository{}); err != nil { return err } else if cnt != 1 { @@ -1898,6 +1909,12 @@ func DeleteRepository(doer *User, uid, repoID int64) error { } if err = sess.Commit(); err != nil { + if len(deployKeys) > 0 { + // We need to rewrite the public keys because the commit failed + if err2 := RewriteAllPublicKeys(); err2 != nil { + return fmt.Errorf("Commit: %v SSH Keys: %v", err, err2) + } + } return fmt.Errorf("Commit: %v", err) } diff --git a/models/ssh_key.go b/models/ssh_key.go index 90c0f04b7..a7dced841 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -51,7 +51,7 @@ type PublicKey struct { ID int64 `xorm:"pk autoincr"` OwnerID int64 `xorm:"INDEX NOT NULL"` Name string `xorm:"NOT NULL"` - Fingerprint string `xorm:"NOT NULL"` + Fingerprint string `xorm:"INDEX NOT NULL"` Content string `xorm:"TEXT NOT NULL"` Mode AccessMode `xorm:"NOT NULL DEFAULT 2"` Type KeyType `xorm:"NOT NULL DEFAULT 1"` @@ -350,7 +350,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { func checkKeyFingerprint(e Engine, fingerprint string) error { has, err := e.Get(&PublicKey{ Fingerprint: fingerprint, - Type: KeyTypeUser, }) if err != nil { return err @@ -401,12 +400,18 @@ func AddPublicKey(ownerID int64, name, content string, LoginSourceID int64) (*Pu return nil, err } - if err := checkKeyFingerprint(x, fingerprint); err != nil { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + + if err := checkKeyFingerprint(sess, fingerprint); err != nil { return nil, err } // Key name of same user cannot be duplicated. - has, err := x. + has, err := sess. Where("owner_id = ? AND name = ?", ownerID, name). Get(new(PublicKey)) if err != nil { @@ -415,12 +420,6 @@ func AddPublicKey(ownerID int64, name, content string, LoginSourceID int64) (*Pu return nil, ErrKeyNameAlreadyUsed{ownerID, name} } - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return nil, err - } - key := &PublicKey{ OwnerID: ownerID, Name: name, @@ -519,7 +518,7 @@ func UpdatePublicKeyUpdated(id int64) error { } // deletePublicKeys does the actual key deletion but does not update authorized_keys file. -func deletePublicKeys(e *xorm.Session, keyIDs ...int64) error { +func deletePublicKeys(e Engine, keyIDs ...int64) error { if len(keyIDs) == 0 { return nil } @@ -728,24 +727,28 @@ func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey accessMode = AccessModeWrite } + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + pkey := &PublicKey{ Fingerprint: fingerprint, - Mode: accessMode, - Type: KeyTypeDeploy, } - has, err := x.Get(pkey) + has, err := sess.Get(pkey) if err != nil { return nil, err } - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return nil, err - } - - // First time use this deploy key. - if !has { + if has { + if pkey.Type != KeyTypeDeploy { + return nil, ErrKeyAlreadyExist{0, fingerprint, ""} + } + } else { + // First time use this deploy key. + pkey.Mode = accessMode + pkey.Type = KeyTypeDeploy pkey.Content = content pkey.Name = name if err = addKey(sess, pkey); err != nil { @@ -763,8 +766,12 @@ func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey // GetDeployKeyByID returns deploy key by given ID. func GetDeployKeyByID(id int64) (*DeployKey, error) { + return getDeployKeyByID(x, id) +} + +func getDeployKeyByID(e Engine, id int64) (*DeployKey, error) { key := new(DeployKey) - has, err := x.ID(id).Get(key) + has, err := e.ID(id).Get(key) if err != nil { return nil, err } else if !has { @@ -775,11 +782,15 @@ func GetDeployKeyByID(id int64) (*DeployKey, error) { // GetDeployKeyByRepo returns deploy key by given public key ID and repository ID. func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) { + return getDeployKeyByRepo(x, keyID, repoID) +} + +func getDeployKeyByRepo(e Engine, keyID, repoID int64) (*DeployKey, error) { key := &DeployKey{ KeyID: keyID, RepoID: repoID, } - has, err := x.Get(key) + has, err := e.Get(key) if err != nil { return nil, err } else if !has { @@ -802,7 +813,19 @@ func UpdateDeployKey(key *DeployKey) error { // DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed. func DeleteDeployKey(doer *User, id int64) error { - key, err := GetDeployKeyByID(id) + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := deleteDeployKey(sess, doer, id); err != nil { + return err + } + return sess.Commit() +} + +func deleteDeployKey(sess Engine, doer *User, id int64) error { + key, err := getDeployKeyByID(sess, id) if err != nil { if IsErrDeployKeyNotExist(err) { return nil @@ -812,11 +835,11 @@ func DeleteDeployKey(doer *User, id int64) error { // Check if user has access to delete this key. if !doer.IsAdmin { - repo, err := GetRepositoryByID(key.RepoID) + repo, err := getRepositoryByID(sess, key.RepoID) if err != nil { return fmt.Errorf("GetRepositoryByID: %v", err) } - has, err := IsUserRepoAdmin(repo, doer) + has, err := isUserRepoAdmin(sess, repo, doer) if err != nil { return fmt.Errorf("GetUserRepoPermission: %v", err) } else if !has { @@ -824,12 +847,6 @@ func DeleteDeployKey(doer *User, id int64) error { } } - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil { return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err) } @@ -851,13 +868,17 @@ func DeleteDeployKey(doer *User, id int64) error { } } - return sess.Commit() + return nil } // ListDeployKeys returns all deploy keys by given repository ID. func ListDeployKeys(repoID int64) ([]*DeployKey, error) { + return listDeployKeys(x, repoID) +} + +func listDeployKeys(e Engine, repoID int64) ([]*DeployKey, error) { keys := make([]*DeployKey, 0, 5) - return keys, x. + return keys, e. Where("repo_id = ?", repoID). Find(&keys) } diff --git a/modules/private/key.go b/modules/private/key.go index 86d0a730d..1c6511846 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -32,6 +32,31 @@ func UpdateDeployKeyUpdated(keyID int64, repoID int64) error { return nil } +// GetDeployKey check if repo has deploy key +func GetDeployKey(keyID, repoID int64) (*models.DeployKey, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/keys/%d", repoID, keyID) + log.GitLogger.Trace("GetDeployKey: %s", reqURL) + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case 404: + return nil, nil + case 200: + var dKey models.DeployKey + if err := json.NewDecoder(resp.Body).Decode(&dKey); err != nil { + return nil, err + } + return &dKey, nil + default: + return nil, fmt.Errorf("Failed to get deploy key: %s", decodeJSONError(resp).Err) + } +} + // HasDeployKey check if repo has deploy key func HasDeployKey(keyID, repoID int64) (bool, error) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/has-keys/%d", repoID, keyID) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 653d34c93..2d32fac9c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -419,7 +419,7 @@ ssh_helper = Need help? Have a look at GitHub's guide to Need help? Have a look at GitHub's guide about GPG. add_new_key = Add SSH Key add_new_gpg_key = Add GPG Key -ssh_key_been_used = This SSH key is already added to your account. +ssh_key_been_used = This SSH key has already been added to the server. ssh_key_name_used = An SSH key with same name is already added to your account. gpg_key_id_used = A public GPG key with same ID already exists. gpg_no_key_email_found = This GPG key is not usable with any email address associated with your account. diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 2caca887a..2ee1ce009 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -159,6 +159,8 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) { // HandleAddKeyError handle add key error func HandleAddKeyError(ctx *context.APIContext, err error) { switch { + case models.IsErrDeployKeyAlreadyExist(err): + ctx.Error(422, "", "This key has already been added to this repository") case models.IsErrKeyAlreadyExist(err): ctx.Error(422, "", "Key content has been used as non-deploy key") case models.IsErrKeyNameAlreadyUsed(err): diff --git a/routers/private/internal.go b/routers/private/internal.go index ec2281c5c..ee6e1274c 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -82,6 +82,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/repositories/:repoid/keys/:keyid/update", UpdateDeployKey) m.Get("/repositories/:repoid/user/:userid/checkunituser", CheckUnitUser) m.Get("/repositories/:repoid/has-keys/:keyid", HasDeployKey) + m.Get("/repositories/:repoid/keys/:keyid", GetDeployKey) m.Get("/repositories/:repoid/wiki/init", InitWiki) m.Post("/push/update", PushUpdate) m.Get("/protectedbranch/:pbid/:userid", CanUserPush) diff --git a/routers/private/key.go b/routers/private/key.go index 9cc116578..ee22f6ac4 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -72,6 +72,24 @@ func GetUserByKeyID(ctx *macaron.Context) { ctx.JSON(200, user) } +//GetDeployKey chainload to models.GetDeployKey +func GetDeployKey(ctx *macaron.Context) { + repoID := ctx.ParamsInt64(":repoid") + keyID := ctx.ParamsInt64(":keyid") + dKey, err := models.GetDeployKeyByRepo(keyID, repoID) + if err != nil { + if models.IsErrDeployKeyNotExist(err) { + ctx.JSON(404, []byte("not found")) + return + } + ctx.JSON(500, map[string]interface{}{ + "err": err.Error(), + }) + return + } + ctx.JSON(200, dKey) +} + //HasDeployKey chainload to models.HasDeployKey func HasDeployKey(ctx *macaron.Context) { repoID := ctx.ParamsInt64(":repoid") diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 5e9e24f9f..4fb74f6cf 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -622,6 +622,9 @@ func DeployKeysPost(ctx *context.Context, form auth.AddKeyForm) { case models.IsErrDeployKeyAlreadyExist(err): ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) + case models.IsErrKeyAlreadyExist(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) case models.IsErrKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)