]> source.dussan.org Git - gitea.git/commitdiff
Refactor find forks and fix possible bugs that weak permissions check (#32528)
authorLunny Xiao <xiaolunwen@gmail.com>
Mon, 18 Nov 2024 03:06:25 +0000 (19:06 -0800)
committerGitHub <noreply@github.com>
Mon, 18 Nov 2024 03:06:25 +0000 (03:06 +0000)
- Move models/GetForks to services/FindForks
- Add doer as a parameter of FindForks to check permissions
- Slight performance optimization for get forks API with batch loading
of repository units
- Add tests for forking repository to organizations

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
models/repo/fork.go
models/repo/repo_list.go
routers/api/v1/repo/fork.go
routers/web/repo/view.go
services/repository/fork.go
templates/repo/forks.tmpl
tests/integration/api_fork_test.go
tests/integration/repo_fork_test.go

index 07cd31c2690a99bd286d6d26aa02364e2f54d6e5..1c75e86458b2ff1e1f37d250520774519bda886c 100644 (file)
@@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
        return &forkedRepo, nil
 }
 
-// GetForks returns all the forks of the repository
-func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
-       sess := db.GetEngine(ctx)
-
-       var forks []*Repository
-       if listOptions.Page == 0 {
-               forks = make([]*Repository, 0, repo.NumForks)
-       } else {
-               forks = make([]*Repository, 0, listOptions.PageSize)
-               sess = db.SetSessionPagination(sess, &listOptions)
-       }
-
-       return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
-}
-
 // IncrementRepoForkNum increment repository fork number
 func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
        _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
index 1bffadbf0ae42e297978eb8641fa6f45c765d2ce..9bed2e919723b5153d2a59c13554f3941c25899f 100644 (file)
@@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 {
        return repoIDs
 }
 
-// LoadAttributes loads the attributes for the given RepositoryList
-func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+func (repos RepositoryList) LoadOwners(ctx context.Context) error {
        if len(repos) == 0 {
                return nil
        }
@@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
        userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
                return repo.OwnerID, true
        })
-       repoIDs := make([]int64, len(repos))
-       for i := range repos {
-               repoIDs[i] = repos[i].ID
-       }
 
        // Load owners.
        users := make(map[int64]*user_model.User, len(userIDs))
@@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
        for i := range repos {
                repos[i].Owner = users[repos[i].OwnerID]
        }
+       return nil
+}
+
+func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
+       if len(repos) == 0 {
+               return nil
+       }
 
        // Load primary language.
        stats := make(LanguageStatList, 0, len(repos))
        if err := db.GetEngine(ctx).
                Where("`is_primary` = ? AND `language` != ?", true, "other").
-               In("`repo_id`", repoIDs).
+               In("`repo_id`", repos.IDs()).
                Find(&stats); err != nil {
                return fmt.Errorf("find primary languages: %w", err)
        }
@@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
                        }
                }
        }
-
        return nil
 }
 
+// LoadAttributes loads the attributes for the given RepositoryList
+func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+       if err := repos.LoadOwners(ctx); err != nil {
+               return err
+       }
+
+       return repos.LoadLanguageStats(ctx)
+}
+
 // SearchRepoOptions holds the search options
 type SearchRepoOptions struct {
        db.ListOptions
index a1e3c9804ba39167c3b4d5f340024d2f85469ce6..14a1a8d1c4a3ddbe169877f88cf72cd57e14ccfb 100644 (file)
@@ -55,11 +55,20 @@ func ListForks(ctx *context.APIContext) {
        //   "404":
        //     "$ref": "#/responses/notFound"
 
-       forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
+       forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
        if err != nil {
-               ctx.Error(http.StatusInternalServerError, "GetForks", err)
+               ctx.Error(http.StatusInternalServerError, "FindForks", err)
                return
        }
+       if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+               ctx.Error(http.StatusInternalServerError, "LoadOwners", err)
+               return
+       }
+       if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil {
+               ctx.Error(http.StatusInternalServerError, "LoadUnits", err)
+               return
+       }
+
        apiForks := make([]*api.Repository, len(forks))
        for i, fork := range forks {
                permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
@@ -70,7 +79,7 @@ func ListForks(ctx *context.APIContext) {
                apiForks[i] = convert.ToRepo(ctx, fork, permission)
        }
 
-       ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks))
+       ctx.SetTotalCountHeader(total)
        ctx.JSON(http.StatusOK, apiForks)
 }
 
index 7030f6d8a982adc0383a7e2de8c283a88966c465..5d68ace29b535f68360005d83267c3111b1df3ff 100644 (file)
@@ -1151,26 +1151,25 @@ func Forks(ctx *context.Context) {
        if page <= 0 {
                page = 1
        }
+       pageSize := setting.ItemsPerPage
 
-       pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
-       ctx.Data["Page"] = pager
-
-       forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
-               Page:     pager.Paginater.Current(),
-               PageSize: setting.ItemsPerPage,
+       forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
+               Page:     page,
+               PageSize: pageSize,
        })
        if err != nil {
-               ctx.ServerError("GetForks", err)
+               ctx.ServerError("FindForks", err)
                return
        }
 
-       for _, fork := range forks {
-               if err = fork.LoadOwner(ctx); err != nil {
-                       ctx.ServerError("LoadOwner", err)
-                       return
-               }
+       if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+               ctx.ServerError("LoadAttributes", err)
+               return
        }
 
+       pager := context.NewPagination(int(total), pageSize, page, 5)
+       ctx.Data["Page"] = pager
+
        ctx.Data["Forks"] = forks
 
        ctx.HTML(http.StatusOK, tplForks)
index 5b24015a0384f2716c2de6377539a25af756c6a2..bc4fdf85627b010c946b16c2275ccc5a7ec8d2e1 100644 (file)
@@ -12,6 +12,7 @@ import (
        "code.gitea.io/gitea/models/db"
        git_model "code.gitea.io/gitea/models/git"
        repo_model "code.gitea.io/gitea/models/repo"
+       "code.gitea.io/gitea/models/unit"
        user_model "code.gitea.io/gitea/models/user"
        "code.gitea.io/gitea/modules/git"
        "code.gitea.io/gitea/modules/gitrepo"
@@ -20,6 +21,8 @@ import (
        "code.gitea.io/gitea/modules/structs"
        "code.gitea.io/gitea/modules/util"
        notify_service "code.gitea.io/gitea/services/notify"
+
+       "xorm.io/builder"
 )
 
 // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
@@ -247,3 +250,24 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit
 
        return err
 }
+
+type findForksOptions struct {
+       db.ListOptions
+       RepoID int64
+       Doer   *user_model.User
+}
+
+func (opts findForksOptions) ToConds() builder.Cond {
+       return builder.Eq{"fork_id": opts.RepoID}.And(
+               repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
+       )
+}
+
+// FindForks returns all the forks of the repository
+func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) {
+       return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{
+               ListOptions: listOptions,
+               RepoID:      repo.ID,
+               Doer:        doer,
+       })
+}
index 412c59b60e84d291f7fa037a2f593b4ee3f3ae5c..725b67c651cb10d256863d01f94543ef7d6978ca 100644 (file)
@@ -5,12 +5,14 @@
                <h2 class="ui dividing header">
                        {{ctx.Locale.Tr "repo.forks"}}
                </h2>
+               <div class="flex-list">
                {{range .Forks}}
-                       <div class="tw-flex tw-items-center tw-py-2">
-                               <span class="tw-mr-1">{{ctx.AvatarUtils.Avatar .Owner}}</span>
-                               <a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a>
+                       <div class="flex-item tw-border-0 repo-fork-item">
+                               <span>{{ctx.AvatarUtils.Avatar .Owner}}</span>
+                               <span><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a></span>
                        </div>
                {{end}}
+               </div>
        </div>
 
        {{template "base/paginate" .}}
index 7c231415a318a5ef5fc755c772bf6f04f241255d..357dd27f86888747c4d1439b3eee83b18bd5b72d 100644 (file)
@@ -7,8 +7,16 @@ import (
        "net/http"
        "testing"
 
+       "code.gitea.io/gitea/models"
+       auth_model "code.gitea.io/gitea/models/auth"
+       "code.gitea.io/gitea/models/db"
+       org_model "code.gitea.io/gitea/models/organization"
+       "code.gitea.io/gitea/models/unittest"
+       user_model "code.gitea.io/gitea/models/user"
        api "code.gitea.io/gitea/modules/structs"
        "code.gitea.io/gitea/tests"
+
+       "github.com/stretchr/testify/assert"
 )
 
 func TestCreateForkNoLogin(t *testing.T) {
@@ -16,3 +24,75 @@ func TestCreateForkNoLogin(t *testing.T) {
        req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
        MakeRequest(t, req, http.StatusUnauthorized)
 }
+
+func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
+       defer tests.PrepareTestEnv(t)()
+
+       user1Sess := loginUser(t, "user1")
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+       // fork into a limited org
+       limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+       assert.EqualValues(t, api.VisibleTypeLimited, limitedOrg.Visibility)
+
+       ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+       assert.NoError(t, err)
+       assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+       user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+       req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+               Organization: &limitedOrg.Name,
+       }).AddTokenAuth(user1Token)
+       MakeRequest(t, req, http.StatusAccepted)
+
+       // fork into a private org
+       user4Sess := loginUser(t, "user4")
+       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+       privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+       assert.EqualValues(t, api.VisibleTypePrivate, privateOrg.Visibility)
+
+       ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+       assert.NoError(t, err)
+       assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+       user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+       req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+               Organization: &privateOrg.Name,
+       }).AddTokenAuth(user4Token)
+       MakeRequest(t, req, http.StatusAccepted)
+
+       t.Run("Anonymous", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
+               resp := MakeRequest(t, req, http.StatusOK)
+
+               var forks []*api.Repository
+               DecodeJSON(t, resp, &forks)
+
+               assert.Empty(t, forks)
+               assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count"))
+       })
+
+       t.Run("Logged in", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+               resp := MakeRequest(t, req, http.StatusOK)
+
+               var forks []*api.Repository
+               DecodeJSON(t, resp, &forks)
+
+               assert.Len(t, forks, 1)
+               assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
+
+               assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+
+               req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+               resp = MakeRequest(t, req, http.StatusOK)
+
+               forks = []*api.Repository{}
+               DecodeJSON(t, resp, &forks)
+
+               assert.Len(t, forks, 2)
+               assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
+       })
+}
index feebebf062081cc060e62e7390016d76a8fe7f73..52b55888b9dd9e798e9f4b61a3fcbd1a97c4d265 100644 (file)
@@ -9,8 +9,12 @@ import (
        "net/http/httptest"
        "testing"
 
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/models/db"
+       org_model "code.gitea.io/gitea/models/organization"
        "code.gitea.io/gitea/models/unittest"
        user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/structs"
        "code.gitea.io/gitea/tests"
 
        "github.com/stretchr/testify/assert"
@@ -74,3 +78,51 @@ func TestRepoForkToOrg(t *testing.T) {
        _, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
        assert.False(t, exists, "Forking should not be allowed anymore")
 }
+
+func TestForkListLimitedAndPrivateRepos(t *testing.T) {
+       defer tests.PrepareTestEnv(t)()
+       forkItemSelector := ".repo-fork-item"
+
+       user1Sess := loginUser(t, "user1")
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+       // fork to a limited org
+       limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+       assert.EqualValues(t, structs.VisibleTypeLimited, limitedOrg.Visibility)
+       ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+       assert.NoError(t, err)
+       assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+       testRepoFork(t, user1Sess, "user2", "repo1", limitedOrg.Name, "repo1", "")
+
+       // fork to a private org
+       user4Sess := loginUser(t, "user4")
+       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+       privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+       assert.EqualValues(t, structs.VisibleTypePrivate, privateOrg.Visibility)
+       ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+       assert.NoError(t, err)
+       assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+       testRepoFork(t, user4Sess, "user2", "repo1", privateOrg.Name, "repo1", "")
+
+       t.Run("Anonymous", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               req := NewRequest(t, "GET", "/user2/repo1/forks")
+               resp := MakeRequest(t, req, http.StatusOK)
+               htmlDoc := NewHTMLParser(t, resp.Body)
+               assert.EqualValues(t, 0, htmlDoc.Find(forkItemSelector).Length())
+       })
+
+       t.Run("Logged in", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", "/user2/repo1/forks")
+               resp := user1Sess.MakeRequest(t, req, http.StatusOK)
+               htmlDoc := NewHTMLParser(t, resp.Body)
+               assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length())
+
+               assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+               resp = user1Sess.MakeRequest(t, req, http.StatusOK)
+               htmlDoc = NewHTMLParser(t, resp.Body)
+               assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
+       })
+}