From 380d95b0d2289f57edfb3b6c0c482b4e422671f5 Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Sat, 7 Feb 2026 20:25:43 +0100 Subject: [PATCH] lfs: relax lfs checks allowing AGit+LFS --- routers/private/serv.go | 5 +- services/lfs/server.go | 3 +- tests/integration/agit_lfs_test.go | 119 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 tests/integration/agit_lfs_test.go diff --git a/routers/private/serv.go b/routers/private/serv.go index b752556c23..47f9ffc531 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -333,8 +333,9 @@ func ServCommand(ctx *context.PrivateContext) { // Because of the special ref "refs/for" (AGit) we will need to delay write permission check, // AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR). // The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go). - // Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations. - if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == git.CmdVerbReceivePack { + // Here it should relax the permission check for "git push (git-receive-pack)" and LFS upload operations. + if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && + (verb == git.CmdVerbReceivePack || verb == git.CmdVerbLfsAuthenticate || verb == git.CmdVerbLfsTransfer) { mode = perm.AccessModeRead } diff --git a/services/lfs/server.go b/services/lfs/server.go index 10b4dba222..d7e0c0b97b 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/httpauth" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" lfs_module "code.gitea.io/gitea/modules/lfs" @@ -537,7 +538,7 @@ func writeStatusMessage(ctx *context.Context, status int, message string) { // to proceed. This server assumes an HTTP Basic auth format. func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool { accessMode := perm_model.AccessModeRead - if requireWrite { + if requireWrite && !git.DefaultFeatures().SupportProcReceive { accessMode = perm_model.AccessModeWrite } diff --git a/tests/integration/agit_lfs_test.go b/tests/integration/agit_lfs_test.go new file mode 100644 index 0000000000..e53b64f092 --- /dev/null +++ b/tests/integration/agit_lfs_test.go @@ -0,0 +1,119 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func testAPISaveUserPublicKey(t *testing.T, session *TestSession, username, keyname, content string) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", &api.CreateKeyOption{ + Title: keyname, + Key: content, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) +} + +func TestAgitLFS(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // Enable LFS + defer tests.PrepareTestEnv(t)() + setting.LFS.StartServer = true + setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs") + + t.Run("HTTP", func(t *testing.T) { + dstPath := t.TempDir() + + // user4 has read access to repo1 (owned by user2) + u.Path = "user2/repo1.git" + u.User = url.UserPassword("user4", userPassword) + + doGitClone(dstPath, u)(t) + + // Setup LFS in the repo + _, _, err := gitcmd.NewCommand("lfs", "install").WithDir(dstPath).RunStdString(t.Context()) + assert.NoError(t, err) + + _, _, err = gitcmd.NewCommand("lfs", "track", "*.bin").WithDir(dstPath).RunStdString(t.Context()) + assert.NoError(t, err) + + assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "large.bin"), []byte("this is a large file"), 0o644)) + assert.NoError(t, git.AddChanges(t.Context(), dstPath, true)) + + signature := git.Signature{ + Email: "user4@example.com", + Name: "user4", + } + assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{ + Committer: &signature, + Author: &signature, + Message: "Add LFS file", + })) + + // push to create an agit pull request + assert.NoError(t, gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-lfs-http"). + WithDir(dstPath). + Run(t.Context())) + }) + + t.Run("SSH", func(t *testing.T) { + dstPath := t.TempDir() + + // user4 has read access to repo1 (owned by user2) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + sshURL := createSSHUrl(repo.FullName()+".git", u) + + withKeyFile(t, "id_rsa", func(keyFile string) { + t.Run("AddKey", func(t *testing.T) { + session := loginUser(t, "user4") + content, _ := os.ReadFile(keyFile + ".pub") + testAPISaveUserPublicKey(t, session, "user4", "user4-agit-lfs", string(content)) + }) + + doGitClone(dstPath, sshURL)(t) + + // Setup LFS in the repo + _, _, err := gitcmd.NewCommand("lfs", "install").WithDir(dstPath).RunStdString(t.Context()) + assert.NoError(t, err) + + _, _, err = gitcmd.NewCommand("lfs", "track", "*.bin").WithDir(dstPath).RunStdString(t.Context()) + assert.NoError(t, err) + + assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "large-ssh.bin"), []byte("this is a large file via ssh"), 0o644)) + assert.NoError(t, git.AddChanges(t.Context(), dstPath, true)) + + signature := git.Signature{ + Email: "user4@example.com", + Name: "user4", + } + assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{ + Committer: &signature, + Author: &signature, + Message: "Add LFS file via SSH", + })) + + // push to create an agit pull request + assert.NoError(t, gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-lfs-ssh"). + WithDir(dstPath). + Run(t.Context())) + }) + }) + }) +}