diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 928c78a61f..fbfdcf2d51 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -180,8 +180,8 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { } if repoExist { - // Because of special ref "refs/for" (agit) , need delay write permission check - if git.DefaultFeatures().SupportProcReceive { + // Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks. + if git.DefaultFeatures().SupportProcReceive && !isWiki { accessMode = perm.AccessModeRead } diff --git a/services/lfs/server.go b/services/lfs/server.go index 0d4243a47e..26eb4878c6 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "github.com/golang-jwt/jwt/v5" @@ -605,6 +606,18 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err) return nil, err } + if !u.IsActive || u.ProhibitLogin { + return nil, util.NewPermissionDeniedErrorf("not allowed to access any repository") + } + + perm, err := access_model.GetDoerRepoPermission(ctx, target, u) + if err != nil { + log.Error("Unable to GetDoerRepoPermission for user[%d] repo[%d]: %v", claims.UserID, target.ID, err) + return nil, err + } + if !perm.CanAccess(mode, unit.TypeCode) { + return nil, util.NewPermissionDeniedErrorf("no permission to access the repository") + } return u, nil } diff --git a/services/lfs/server_test.go b/services/lfs/server_test.go index e66e48cee8..d8e5a9252a 100644 --- a/services/lfs/server_test.go +++ b/services/lfs/server_test.go @@ -7,9 +7,11 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" perm_model "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" @@ -22,11 +24,15 @@ func TestMain(m *testing.M) { func TestAuthenticate(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) + ctx, _ := contexttest.MockContext(t, "/") + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1}) - _, token2, _ = strings.Cut(token2, " ") - ctx, _ := contexttest.MockContext(t, "/") + getUserToken := func(op string, userID int64, repo *repo_model.Repository) string { + s, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: op, UserID: userID, RepoID: repo.ID}) + _, token, _ := strings.Cut(s, " ") + return token + } t.Run("handleLFSToken", func(t *testing.T) { u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead) @@ -37,15 +43,62 @@ func TestAuthenticate(t *testing.T) { require.Error(t, err) assert.Nil(t, u) - u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead) + u, err = handleLFSToken(ctx, getUserToken("download", 2, repo1), repo1, perm_model.AccessModeRead) require.NoError(t, err) assert.EqualValues(t, 2, u.ID) }) t.Run("authenticate", func(t *testing.T) { const prefixBearer = "Bearer " + token := getUserToken("download", 2, repo1) assert.False(t, authenticate(ctx, repo1, "", true, false)) assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false)) - assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false)) + assert.True(t, authenticate(ctx, repo1, prefixBearer+token, true, false)) + }) + + handleLFSTokenTestPerm := func(op string, userID int64, repo *repo_model.Repository, accessMode perm_model.AccessMode) error { + token := getUserToken(op, userID, repo) + u, err := handleLFSToken(ctx, token, repo, accessMode) + if err == nil { + assert.Equal(t, userID, u.ID) + } + return err + } + + t.Run("handleLFSToken blocks prohibited users", func(t *testing.T) { + user37 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 37}) + + // prohibited user + assert.True(t, user37.ProhibitLogin) + err := handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead) + assert.ErrorContains(t, err, "not allowed to access any repository") + + // normal user + _, _ = db.GetEngine(t.Context()).ID(37).Cols("prohibit_login").Update(&user_model.User{ProhibitLogin: false}) + err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead) + assert.NoError(t, err) + + // inactive user + _, _ = db.GetEngine(t.Context()).ID(37).Cols("is_active").Update(&user_model.User{IsActive: false}) + err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead) + assert.ErrorContains(t, err, "not allowed to access any repository") + }) + + t.Run("handleLFSToken blocks users without repo access", func(t *testing.T) { + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + err := handleLFSTokenTestPerm("download", 10, repo2, perm_model.AccessModeRead) + assert.ErrorContains(t, err, "no permission to access the repository") + }) + + t.Run("handleLFSToken requires write access for uploads", func(t *testing.T) { + err := handleLFSTokenTestPerm("download", 10, repo1, perm_model.AccessModeRead) + assert.NoError(t, err) + err = handleLFSTokenTestPerm("upload", 10, repo1, perm_model.AccessModeWrite) + assert.ErrorContains(t, err, "no permission to access the repository") + }) + + t.Run("handleLFSToken allows writes for authorized users", func(t *testing.T) { + err := handleLFSTokenTestPerm("upload", 2, repo1, perm_model.AccessModeWrite) + assert.NoError(t, err) }) } diff --git a/tests/integration/wiki_test.go b/tests/integration/wiki_test.go index 5718156ffa..71f64e98b7 100644 --- a/tests/integration/wiki_test.go +++ b/tests/integration/wiki_test.go @@ -13,52 +13,17 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/git" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func assertFileExist(t *testing.T, p string) { - exist, err := util.IsExist(p) - assert.NoError(t, err) - assert.True(t, exist) -} - -func assertFileEqual(t *testing.T, p string, content []byte) { - bs, err := os.ReadFile(p) - assert.NoError(t, err) - assert.Equal(t, content, bs) -} - -func TestRepoCloneWiki(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - defer tests.PrepareTestEnv(t)() - - dstPath := t.TempDir() - - r := u.String() + "user2/repo1.wiki.git" - u, _ = url.Parse(r) - u.User = url.UserPassword("user2", userPassword) - t.Run("Clone", func(t *testing.T) { - assert.NoError(t, git.Clone(t.Context(), u.String(), dstPath, git.CloneRepoOptions{})) - assertFileEqual(t, filepath.Join(dstPath, "Home.md"), []byte("# Home page\n\nThis is the home page!\n")) - assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md")) - assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md")) - assertFileExist(t, filepath.Join(dstPath, "images")) - assertFileExist(t, filepath.Join(dstPath, "files/Non-Renderable-File.zip")) - assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg")) - }) - }) -} - -func Test_RepoWikiPages(t *testing.T) { +func TestRepoWikiPages(t *testing.T) { defer tests.PrepareTestEnv(t)() - - url := "/user2/repo1/wiki/?action=_pages" - req := NewRequest(t, "GET", url) + req := NewRequest(t, "GET", "/user2/repo1/wiki/?action=_pages") resp := MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) @@ -74,45 +39,72 @@ func Test_RepoWikiPages(t *testing.T) { }) } -func Test_WikiClone(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - username := "user2" - reponame := "repo1" - wikiPath := username + "/" + reponame + ".wiki.git" - keyname := "my-testing-key" - baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) +func testRepoWikiCloneHTTP(t *testing.T, u *url.URL) { + // When proc-receive support is enabled globally, the HTTP receive-pack pre-check + // must still require write access for wiki repositories. Exercise this with a + // normal wiki push because the regression is about the pre-check, not agit refs. + require.True(t, git.DefaultFeatures().SupportProcReceive) // modern git should all support proc-receive - u.Path = wikiPath + wikiURL := *u + wikiURL.Path = "/user2/repo1.wiki.git" - t.Run("Clone HTTP", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + dstLocalPath := t.TempDir() - dstLocalPath := t.TempDir() - assert.NoError(t, git.Clone(t.Context(), u.String(), dstLocalPath, git.CloneRepoOptions{})) - content, err := os.ReadFile(filepath.Join(dstLocalPath, "Home.md")) - assert.NoError(t, err) - assert.Equal(t, "# Home page\n\nThis is the home page!\n", string(content)) - }) + // reader can clone + wikiURL.User = url.UserPassword("user20", userPassword) + require.NoError(t, git.Clone(t.Context(), wikiURL.String(), dstLocalPath, git.CloneRepoOptions{})) + _, _, runErr := gitcmd.NewCommand("fast-import").WithDir(dstLocalPath).WithStdinBytes([]byte(`commit refs/heads/master +committer unauthorized-user 1714310400 +0000 +data <