diff --git a/routers/private/serv.go b/routers/private/serv.go index ef3920d359..e04bcddb52 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -296,8 +296,14 @@ func ServCommand(ctx *context.PrivateContext) { return } } else { - // Because of the special ref "refs/for" we will need to delay write permission check - if git.SupportProcReceive && unitType == unit.TypeCode { + // We don't know yet which references the Git client wants to write to, + // but for AGit flow we have to degrade this check to a read permission. + // So if we support proc-receive (needed for AGit flow) and the unit type + // is code and we know the Git client wants to write to us, then degrade + // the permission check to read. The pre-receive hook will do another + // permission check which ensure for non AGit flow references the write + // permission is checked. + if git.SupportProcReceive && unitType == unit.TypeCode && ctx.FormString("verb") == "git-receive-pack" { mode = perm.AccessModeRead } diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index eb96baca6f..f448736966 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -5,15 +5,18 @@ package integration import ( "bytes" + "context" "encoding/hex" "fmt" "math/rand" "net/http" "net/url" "os" + "os/exec" "path" "path/filepath" "strconv" + "strings" "testing" "time" @@ -34,6 +37,7 @@ import ( files_service "code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/tests" + "github.com/kballard/go-shellquote" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -109,7 +113,10 @@ func testGit(t *testing.T, u *url.URL) { // Setup key the user ssh key withKeyFile(t, keyname, func(keyFile string) { - t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) + var publicKeyID int64 + t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile, func(t *testing.T, pk api.PublicKey) { + publicKeyID = pk.ID + })) // Setup remote link // TODO: get url from api @@ -136,6 +143,7 @@ func testGit(t *testing.T, u *url.URL) { }) t.Run("PushCreate", doPushCreate(sshContext, sshURL)) + t.Run("LFS no access", doLFSNoAccess(sshContext, publicKeyID)) }) }) } @@ -1118,3 +1126,26 @@ func TestDataAsync_Issue29101(t *testing.T) { defer r2.Close() }) } + +func doLFSNoAccess(ctx APITestContext, publicKeyID int64) func(*testing.T) { + return func(t *testing.T) { + // This is set in withKeyFile + sshCommand := os.Getenv("GIT_SSH_COMMAND") + + // Sanity check, because we are going to execute whatever is in here. + require.True(t, strings.HasPrefix(sshCommand, "ssh ")) + + // We really have to split on the arguments and pass them individually. + sshOptions, err := shellquote.Split(strings.TrimPrefix(sshCommand, "ssh ")) + require.NoError(t, err) + + sshOptions = append(sshOptions, "-p "+strconv.Itoa(setting.SSH.ListenPort), "git@"+setting.SSH.ListenHost) + + cmd := exec.CommandContext(context.Background(), "ssh", append(sshOptions, "git-lfs-authenticate", "user40/repo60.git", "upload")...) + stderr := bytes.Buffer{} + cmd.Stderr = &stderr + + require.ErrorContains(t, cmd.Run(), "exit status 1") + assert.Contains(t, stderr.String(), fmt.Sprintf("Forgejo: User: 2:user2 with Key: %d:test-key is not authorized to write to user40/repo60.", publicKeyID)) + } +}