mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-24 08:57:03 -05:00
[FIX] Don't allow SSH authentication without ssh executable
- Follow up of #4819 - When no `ssh` executable is present, disable the UI and backend bits that allow the creation of push mirrors that use SSH authentication. As this feature requires the usage of the `ssh` binary. - Integration test added.
This commit is contained in:
parent
a5b51e9145
commit
1a68d14cf8
7 changed files with 80 additions and 0 deletions
|
@ -38,6 +38,8 @@ var (
|
|||
InvertedGitFlushEnv bool // 2.43.1
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
|
||||
HasSSHExecutable bool
|
||||
|
||||
gitVersion *version.Version
|
||||
)
|
||||
|
||||
|
@ -203,6 +205,10 @@ func InitFull(ctx context.Context) (err error) {
|
|||
globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
|
||||
}
|
||||
|
||||
// Detect the presence of the ssh executable in $PATH.
|
||||
_, err = exec.LookPath("ssh")
|
||||
HasSSHExecutable = err == nil
|
||||
|
||||
return syncGitConfig()
|
||||
}
|
||||
|
||||
|
|
|
@ -1105,6 +1105,7 @@ mirror_interval_invalid = The mirror interval is not valid.
|
|||
mirror_public_key = Public SSH key
|
||||
mirror_use_ssh.text = Use SSH authentication
|
||||
mirror_use_ssh.helper = Forgejo will mirror the repository via Git over SSH and create a keypair for you when you select this option. You must ensure that the generated public key is authorized to push to the destination repository. You cannot use password-based authorization when selecting this.
|
||||
mirror_use_ssh.not_available = SSH authentication isn't available.
|
||||
mirror_denied_combination = Cannot use public key and password based authentication in combination.
|
||||
mirror_sync = synced
|
||||
mirror_sync_on_commit = Sync when commits are pushed
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -350,6 +351,11 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
|
|||
return
|
||||
}
|
||||
|
||||
if mirrorOption.UseSSH && !git.HasSSHExecutable {
|
||||
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "SSH authentication not available.")
|
||||
return
|
||||
}
|
||||
|
||||
if mirrorOption.UseSSH && (mirrorOption.RemoteUsername != "" || mirrorOption.RemotePassword != "") {
|
||||
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "'use_ssh' is mutually exclusive with 'remote_username' and 'remote_passoword'")
|
||||
return
|
||||
|
|
|
@ -92,6 +92,7 @@ func SettingsCtxData(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
ctx.Data["PushMirrors"] = pushMirrors
|
||||
ctx.Data["CanUseSSHMirroring"] = git.HasSSHExecutable
|
||||
}
|
||||
|
||||
// Units show a repositorys unit settings page
|
||||
|
@ -643,6 +644,11 @@ func SettingsPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if form.PushMirrorUseSSH && !git.HasSSHExecutable {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.mirror_use_ssh.not_available"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
|
||||
address, err := forms.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
|
||||
if err == nil {
|
||||
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
|
||||
|
|
|
@ -300,6 +300,7 @@
|
|||
<label for="push_mirror_password">{{ctx.Locale.Tr "password"}}</label>
|
||||
<input id="push_mirror_password" name="push_mirror_password" type="password" value="{{.push_mirror_password}}" autocomplete="off">
|
||||
</div>
|
||||
{{if .CanUseSSHMirroring}}
|
||||
<div class="inline field {{if .Err_PushMirrorUseSSH}}error{{end}}">
|
||||
<div class="ui checkbox df ac">
|
||||
<input id="push_mirror_use_ssh" name="push_mirror_use_ssh" type="checkbox" {{if .push_mirror_use_ssh}}checked{{end}}>
|
||||
|
@ -307,6 +308,7 @@
|
|||
<span class="help tw-block">{{ctx.Locale.Tr "repo.mirror_use_ssh.helper"}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
<div class="field">
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
@ -23,6 +24,7 @@ import (
|
|||
"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/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -141,6 +143,11 @@ func testAPIPushMirror(t *testing.T, u *url.URL) {
|
|||
}
|
||||
|
||||
func TestAPIPushMirrorSSH(t *testing.T) {
|
||||
_, err := exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
t.Skip("SSH executable not present")
|
||||
}
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
|
@ -178,6 +185,22 @@ func TestAPIPushMirrorSSH(t *testing.T) {
|
|||
assert.EqualValues(t, "'use_ssh' is mutually exclusive with 'remote_username' and 'remote_passoword'", apiError.Message)
|
||||
})
|
||||
|
||||
t.Run("SSH not available", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&git.HasSSHExecutable, false)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/push_mirrors", srcRepo.FullName()), &api.CreatePushMirrorOption{
|
||||
RemoteAddress: sshURL,
|
||||
Interval: "8h",
|
||||
UseSSH: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.EqualValues(t, "SSH authentication not available.", apiError.Message)
|
||||
})
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
var pushMirror *repo_model.PushMirror
|
||||
t.Run("Adding", func(t *testing.T) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
@ -157,6 +158,11 @@ func doRemovePushMirror(ctx APITestContext, address, username, password string,
|
|||
}
|
||||
|
||||
func TestSSHPushMirror(t *testing.T) {
|
||||
_, err := exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
t.Skip("SSH executable not present")
|
||||
}
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
|
@ -194,6 +200,36 @@ func TestSSHPushMirror(t *testing.T) {
|
|||
assert.Contains(t, errMsg, "Cannot use public key and password based authentication in combination.")
|
||||
})
|
||||
|
||||
inputSelector := `input[id="push_mirror_use_ssh"]`
|
||||
|
||||
t.Run("SSH not available", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&git.HasSSHExecutable, false)()
|
||||
|
||||
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",
|
||||
})
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
errMsg := htmlDoc.Find(".ui.negative.message").Text()
|
||||
assert.Contains(t, errMsg, "SSH authentication isn't available.")
|
||||
|
||||
htmlDoc.AssertElement(t, inputSelector, false)
|
||||
})
|
||||
|
||||
t.Run("SSH available", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/settings", srcRepo.FullName()))
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, inputSelector, true)
|
||||
})
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
var pushMirror *repo_model.PushMirror
|
||||
t.Run("Adding", func(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue