Merge pull request 'Allow pushmirror to use publickey authentication' (#4819) from ironmagma/forgejo:publickey-auth-push-mirror into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4819 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
5dbacb70f4
24 changed files with 648 additions and 66 deletions
|
@ -7,21 +7,30 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -130,3 +139,130 @@ func testAPIPushMirror(t *testing.T, u *url.URL) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIPushMirrorSSH(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
|
||||
require.NoError(t, migrations.Init())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.False(t, srcRepo.HasWiki())
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
pushToRepo, _, f := CreateDeclarativeRepoWithOptions(t, user, DeclarativeRepoOptions{
|
||||
Name: optional.Some("push-mirror-test"),
|
||||
AutoInit: optional.Some(false),
|
||||
EnabledUnits: optional.Some([]unit.Type{unit.TypeCode}),
|
||||
})
|
||||
defer f()
|
||||
|
||||
sshURL := fmt.Sprintf("ssh://%s@%s/%s.git", setting.SSH.User, net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)), pushToRepo.FullName())
|
||||
|
||||
t.Run("Mutual exclusive", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/push_mirrors", srcRepo.FullName()), &api.CreatePushMirrorOption{
|
||||
RemoteAddress: sshURL,
|
||||
Interval: "8h",
|
||||
UseSSH: true,
|
||||
RemoteUsername: "user",
|
||||
RemotePassword: "password",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.EqualValues(t, "'use_ssh' is mutually exclusive with 'remote_username' and 'remote_passoword'", apiError.Message)
|
||||
})
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
var pushMirror *repo_model.PushMirror
|
||||
t.Run("Adding", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/push_mirrors", srcRepo.FullName()), &api.CreatePushMirrorOption{
|
||||
RemoteAddress: sshURL,
|
||||
Interval: "8h",
|
||||
UseSSH: true,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
pushMirror = unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{RepoID: srcRepo.ID})
|
||||
assert.NotEmpty(t, pushMirror.PrivateKey)
|
||||
assert.NotEmpty(t, pushMirror.PublicKey)
|
||||
})
|
||||
|
||||
publickey := pushMirror.GetPublicKey()
|
||||
t.Run("Publickey", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/push_mirrors", srcRepo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var pushMirrors []*api.PushMirror
|
||||
DecodeJSON(t, resp, &pushMirrors)
|
||||
assert.Len(t, pushMirrors, 1)
|
||||
assert.EqualValues(t, publickey, pushMirrors[0].PublicKey)
|
||||
})
|
||||
|
||||
t.Run("Add deploy key", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/keys", pushToRepo.FullName()), &api.CreateKeyOption{
|
||||
Title: "push mirror key",
|
||||
Key: publickey,
|
||||
ReadOnly: false,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "push mirror key", RepoID: pushToRepo.ID})
|
||||
})
|
||||
|
||||
t.Run("Synchronize", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/push_mirrors-sync", srcRepo.FullName())).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Check mirrored content", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
sha := "1032bbf17fbc0d9c95bb5418dabe8f8c99278700"
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/commits?limit=1", srcRepo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var commitList []*api.Commit
|
||||
DecodeJSON(t, resp, &commitList)
|
||||
|
||||
assert.Len(t, commitList, 1)
|
||||
assert.EqualValues(t, sha, commitList[0].SHA)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/commits?limit=1", srcRepo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var commitList []*api.Commit
|
||||
DecodeJSON(t, resp, &commitList)
|
||||
|
||||
return len(commitList) != 0 && commitList[0].SHA == sha
|
||||
}, time.Second*30, time.Second)
|
||||
})
|
||||
|
||||
t.Run("Check known host keys", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
knownHosts, err := os.ReadFile(filepath.Join(setting.SSH.RootPath, "known_hosts"))
|
||||
require.NoError(t, err)
|
||||
|
||||
publicKey, err := os.ReadFile(setting.SSH.ServerHostKeys[0] + ".pub")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(knownHosts), string(publicKey))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
@ -6,18 +7,26 @@ package integration
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
doctor "code.gitea.io/gitea/services/doctor"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
|
@ -35,8 +44,8 @@ func TestMirrorPush(t *testing.T) {
|
|||
|
||||
func testMirrorPush(t *testing.T, u *url.URL) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
|
||||
setting.Migrations.AllowLocalNetworks = true
|
||||
require.NoError(t, migrations.Init())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
@ -146,3 +155,135 @@ func doRemovePushMirror(ctx APITestContext, address, username, password string,
|
|||
assert.Contains(t, flashCookie.Value, "success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHPushMirror(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
|
||||
require.NoError(t, migrations.Init())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.False(t, srcRepo.HasWiki())
|
||||
sess := loginUser(t, user.Name)
|
||||
pushToRepo, _, f := CreateDeclarativeRepoWithOptions(t, user, DeclarativeRepoOptions{
|
||||
Name: optional.Some("push-mirror-test"),
|
||||
AutoInit: optional.Some(false),
|
||||
EnabledUnits: optional.Some([]unit.Type{unit.TypeCode}),
|
||||
})
|
||||
defer f()
|
||||
|
||||
sshURL := fmt.Sprintf("ssh://%s@%s/%s.git", setting.SSH.User, net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)), pushToRepo.FullName())
|
||||
t.Run("Mutual exclusive", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-add",
|
||||
"push_mirror_address": sshURL,
|
||||
"push_mirror_username": "username",
|
||||
"push_mirror_password": "password",
|
||||
"push_mirror_use_ssh": "true",
|
||||
"push_mirror_interval": "0",
|
||||
})
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
errMsg := htmlDoc.Find(".ui.negative.message").Text()
|
||||
assert.Contains(t, errMsg, "Cannot use public key and password based authentication in combination.")
|
||||
})
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
var pushMirror *repo_model.PushMirror
|
||||
t.Run("Adding", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-add",
|
||||
"push_mirror_address": sshURL,
|
||||
"push_mirror_use_ssh": "true",
|
||||
"push_mirror_interval": "0",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
flashCookie := sess.GetCookie(gitea_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Contains(t, flashCookie.Value, "success")
|
||||
|
||||
pushMirror = unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{RepoID: srcRepo.ID})
|
||||
assert.NotEmpty(t, pushMirror.PrivateKey)
|
||||
assert.NotEmpty(t, pushMirror.PublicKey)
|
||||
})
|
||||
|
||||
publickey := ""
|
||||
t.Run("Publickey", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/settings", srcRepo.FullName()))
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
publickey = htmlDoc.Find(".ui.table td a[data-clipboard-text]").AttrOr("data-clipboard-text", "")
|
||||
assert.EqualValues(t, publickey, pushMirror.GetPublicKey())
|
||||
})
|
||||
|
||||
t.Run("Add deploy key", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings/keys", pushToRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings/keys", pushToRepo.FullName())),
|
||||
"title": "push mirror key",
|
||||
"content": publickey,
|
||||
"is_writable": "true",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "push mirror key", RepoID: pushToRepo.ID})
|
||||
})
|
||||
|
||||
t.Run("Synchronize", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-sync",
|
||||
"push_mirror_id": strconv.FormatInt(pushMirror.ID, 10),
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Check mirrored content", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
shortSHA := "1032bbf17f"
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s", srcRepo.FullName()))
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
assert.Contains(t, htmlDoc.Find(".shortsha").Text(), shortSHA)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s", pushToRepo.FullName()))
|
||||
resp = sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
return htmlDoc.Find(".shortsha").Text() == shortSHA
|
||||
}, time.Second*30, time.Second)
|
||||
})
|
||||
|
||||
t.Run("Check known host keys", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
knownHosts, err := os.ReadFile(filepath.Join(setting.SSH.RootPath, "known_hosts"))
|
||||
require.NoError(t, err)
|
||||
|
||||
publicKey, err := os.ReadFile(setting.SSH.ServerHostKeys[0] + ".pub")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(knownHosts), string(publicKey))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue