Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor find forks and fix possible bugs that weak permissions check #32528

Merged
merged 8 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions models/repo/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 18 additions & 8 deletions models/repo/repo_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,14 @@ 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
}

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))
Expand All @@ -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)
}
Expand All @@ -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
Expand Down
15 changes: 12 additions & 3 deletions routers/api/v1/repo/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down
23 changes: 11 additions & 12 deletions routers/web/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion services/mirror/mirror_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
output := stderrBuilder.String()

if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err)
log.Error("SyncMirrors: WriteCommitGraph [repo: %-v]: %v", m.Repo, err)
}

gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
Expand Down
23 changes: 23 additions & 0 deletions services/repository/fork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,6 +21,7 @@ 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.
Expand Down Expand Up @@ -247,3 +249,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,
})
}
96 changes: 96 additions & 0 deletions tests/integration/api_fork_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,108 @@
"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"
"code.gitea.io/gitea/modules/structs"

Check failure on line 16 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

ST1019: package "code.gitea.io/gitea/modules/structs" is being imported more than once (stylecheck)

Check failure on line 16 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

ST1019: package "code.gitea.io/gitea/modules/structs" is being imported more than once (stylecheck)

Check failure on line 16 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

ST1019: package "code.gitea.io/gitea/modules/structs" is being imported more than once (stylecheck)
api "code.gitea.io/gitea/modules/structs"

Check failure on line 17 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

duplicated-imports: Package "code.gitea.io/gitea/modules/structs" already imported (revive)

Check failure on line 17 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

duplicated-imports: Package "code.gitea.io/gitea/modules/structs" already imported (revive)

Check failure on line 17 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

duplicated-imports: Package "code.gitea.io/gitea/modules/structs" already imported (revive)
"code.gitea.io/gitea/tests"

"github.com/stretchr/testify/assert"
)

func TestCreateForkNoLogin(t *testing.T) {
defer tests.PrepareTestEnv(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"})
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))
defer func() {
models.RemoveTeamMember(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)

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))
defer func() {
models.RemoveTeamMember(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("Anomynous", func(t *testing.T) {

Check failure on line 75 in tests/integration/api_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-spell

"Anomynous" is a misspelling of "Anonymous"
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))
defer func() {
models.RemoveTeamMember(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"))
})
}
66 changes: 66 additions & 0 deletions tests/integration/repo_fork_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"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"
Expand Down Expand Up @@ -74,3 +79,64 @@
_, 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) {
forkItemSelector := ".tw-flex.tw-items-center.tw-py-2"
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved

onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1Sess := loginUser(t, "user1")
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
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))
defer func() {
models.RemoveTeamMember(db.DefaultContext, ownerTeam1, user1)
}()

testRepoFork(t, user1Sess, "user2", "repo1", limitedOrg.Name, "repo1", "")

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))
defer func() {
models.RemoveTeamMember(db.DefaultContext, ownerTeam2, user4)
}()

testRepoFork(t, user4Sess, "user2", "repo1", privateOrg.Name, "repo1", "")

t.Run("Anomynous", func(t *testing.T) {

Check failure on line 116 in tests/integration/repo_fork_test.go

View workflow job for this annotation

GitHub Actions / lint-spell

"Anomynous" is a misspelling of "Anonymous"
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))
defer func() {
models.RemoveTeamMember(db.DefaultContext, ownerTeam2, user1)
}()
resp = user1Sess.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
})
})
}
Loading