mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-05 14:46:19 +00:00
de4b8277e9
Backport #38406 by @bircni Addresses a batch of privately reported security issues, grouped by area: - **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID discovery, pull-mirror URL re-validation, and the outbound proxy path. - **Access-token scope** - prevent scope escalation on token creation; keep public-only tokens confined (feeds, packages, Actions listings, star/watch lists, limited/private owners). - **Access control / disclosure** - go-get default-branch leak, webhook authorization-header leak, watch clearing on private transitions, label/attachment scoping. - **Denial of service** - input bounds for npm dist-tags, Debian control files, Arch file lists, and SSH keys. ### 📌 Attention for site admins Not breaking - existing configs keep working - but two changes are worth a look: - **New SSRF protection** Outbound requests (migrations, OAuth2 avatars, OpenID discovery, pull mirrors, proxy path) are now validated against the allow/block host lists. If your instance legitimately reaches internal hosts, you may need to add them to `[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS` settings). - **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will be removed in a future release. Use `[security].ALLOWED_HOST_LIST` instead; the old key still works for now. Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
517 lines
19 KiB
Go
517 lines
19 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/optional"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/structs"
|
|
"gitea.dev/modules/test"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func getTestCases() []struct {
|
|
name string
|
|
opts repo_model.SearchRepoOptions
|
|
count int
|
|
} {
|
|
testCases := []struct {
|
|
name string
|
|
opts repo_model.SearchRepoOptions
|
|
count int
|
|
}{
|
|
{
|
|
name: "PublicRepositoriesByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)},
|
|
count: 7,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfUser",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfUser2",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)},
|
|
count: 0,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfOrg3",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfUser",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)},
|
|
count: 4,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfUser2",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)},
|
|
count: 0,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfOrg3",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)},
|
|
count: 4,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfUserIncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15},
|
|
count: 5,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfUser2IncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18},
|
|
count: 1,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfOrg3IncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20},
|
|
count: 3,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true},
|
|
count: 9,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true},
|
|
count: 4,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfOrg3IncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true},
|
|
count: 7,
|
|
},
|
|
{
|
|
name: "PublicRepositoriesOfOrganization",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)},
|
|
count: 1,
|
|
},
|
|
{
|
|
name: "PublicAndPrivateRepositoriesOfOrganization",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicRepositoriesByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)},
|
|
count: 7,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicAndPrivateRepositoriesByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)},
|
|
count: 14,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
|
|
count: 34,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
|
|
count: 39,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true},
|
|
count: 15,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true},
|
|
count: 13,
|
|
},
|
|
{
|
|
name: "AllPublic/PublicRepositoriesOfOrganization",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
|
|
count: 34,
|
|
},
|
|
{
|
|
name: "AllTemplates",
|
|
opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "OwnerSlashRepoSearch",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "OwnerSlashSearch",
|
|
opts: repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0},
|
|
count: 4,
|
|
},
|
|
}
|
|
|
|
return testCases
|
|
}
|
|
|
|
func TestSearchRepository(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
t.Run("SearchRepositoryPublic", testSearchRepositoryPublic)
|
|
t.Run("SearchRepositoryPublicRestricted", testSearchRepositoryRestricted)
|
|
t.Run("SearchRepositoryPrivate", testSearchRepositoryPrivate)
|
|
t.Run("SearchRepositoryNonExistingOwner", testSearchRepositoryNonExistingOwner)
|
|
t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription)
|
|
t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription)
|
|
t.Run("SearchRepositoryCases", testSearchRepositoryCases)
|
|
}
|
|
|
|
func testSearchRepositoryPublic(t *testing.T) {
|
|
// test search public repository on explore page
|
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "repo_12",
|
|
Collaborate: optional.Some(false),
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
if assert.Len(t, repos, 1) {
|
|
assert.Equal(t, "test_repo_12", repos[0].Name)
|
|
}
|
|
assert.Equal(t, int64(1), count)
|
|
|
|
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "test_repo",
|
|
Collaborate: optional.Some(false),
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(2), count)
|
|
assert.Len(t, repos, 2)
|
|
}
|
|
|
|
func testSearchRepositoryRestricted(t *testing.T) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
|
|
|
|
performSearch := func(t *testing.T, user *user_model.User) (publicRepoIDs []int64) {
|
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{Page: 1, PageSize: 10000},
|
|
Actor: user,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, repos, int(count))
|
|
for _, repo := range repos {
|
|
require.NoError(t, repo.LoadOwner(t.Context()))
|
|
if repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate {
|
|
publicRepoIDs = append(publicRepoIDs, repo.ID)
|
|
}
|
|
}
|
|
return publicRepoIDs
|
|
}
|
|
|
|
normalPublicRepoIDs := performSearch(t, user2)
|
|
require.Greater(t, len(normalPublicRepoIDs), 10) // quite a lot
|
|
|
|
t.Run("RestrictedUser-NoSignInRequirement", func(t *testing.T) {
|
|
// restricted user can also see public repositories if no "required sign-in"
|
|
repoIDs := performSearch(t, restrictedUser)
|
|
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
|
|
})
|
|
|
|
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
|
|
|
t.Run("NormalUser-RequiredSignIn", func(t *testing.T) {
|
|
// normal user can still see all public repos, not affected by "required sign-in"
|
|
repoIDs := performSearch(t, user2)
|
|
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
|
|
})
|
|
t.Run("RestrictedUser-RequiredSignIn", func(t *testing.T) {
|
|
// restricted user can see only their own repo
|
|
repoIDs := performSearch(t, restrictedUser)
|
|
assert.Equal(t, []int64{4}, repoIDs)
|
|
})
|
|
}
|
|
|
|
func testSearchRepositoryPrivate(t *testing.T) {
|
|
// test search private repository on explore page
|
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "repo_13",
|
|
Private: true,
|
|
Collaborate: optional.Some(false),
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
if assert.Len(t, repos, 1) {
|
|
assert.Equal(t, "test_repo_13", repos[0].Name)
|
|
}
|
|
assert.Equal(t, int64(1), count)
|
|
|
|
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "test_repo",
|
|
Private: true,
|
|
Collaborate: optional.Some(false),
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
assert.Len(t, repos, 3)
|
|
}
|
|
|
|
func testSearchRepositoryNonExistingOwner(t *testing.T) {
|
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, repos)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
func testSearchRepositoryWithInDescription(t *testing.T) {
|
|
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "description_14",
|
|
Collaborate: optional.Some(false),
|
|
IncludeDescription: true,
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
if assert.Len(t, repos, 1) {
|
|
assert.Equal(t, "test_repo_14", repos[0].Name)
|
|
}
|
|
assert.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func testSearchRepositoryNotInDescription(t *testing.T) {
|
|
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
},
|
|
Keyword: "description_14",
|
|
Collaborate: optional.Some(false),
|
|
IncludeDescription: false,
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, repos)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
func testSearchRepositoryCases(t *testing.T) {
|
|
testCases := getTestCases()
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), testCase.opts)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(testCase.count), count)
|
|
|
|
page := testCase.opts.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
expectedLen := testCase.opts.PageSize
|
|
if testCase.opts.PageSize*page > testCase.count+testCase.opts.PageSize {
|
|
expectedLen = 0
|
|
} else if testCase.opts.PageSize*page > testCase.count {
|
|
expectedLen = testCase.count % testCase.opts.PageSize
|
|
}
|
|
if assert.Len(t, repos, expectedLen) {
|
|
for _, repo := range repos {
|
|
assert.NotEmpty(t, repo.Name)
|
|
|
|
if len(testCase.opts.Keyword) > 0 {
|
|
// Keyword match condition is different for search terms of form "owner/repo"
|
|
if strings.Count(testCase.opts.Keyword, "/") == 1 {
|
|
// May still match as a whole...
|
|
wholeMatch := strings.Contains(repo.Name, testCase.opts.Keyword)
|
|
|
|
pieces := strings.Split(testCase.opts.Keyword, "/")
|
|
ownerName := pieces[0]
|
|
repoName := pieces[1]
|
|
// ... or match in parts
|
|
splitMatch := strings.Contains(repo.OwnerName, ownerName) && strings.Contains(repo.Name, repoName)
|
|
|
|
assert.True(t, wholeMatch || splitMatch, "Keyword '%s' does not match repo '%s/%s'", testCase.opts.Keyword, repo.Owner.Name, repo.Name)
|
|
} else {
|
|
assert.Contains(t, repo.Name, testCase.opts.Keyword)
|
|
}
|
|
}
|
|
|
|
if !testCase.opts.Private {
|
|
assert.False(t, repo.IsPrivate)
|
|
}
|
|
|
|
if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
|
|
assert.True(t, repo.IsFork && repo.IsMirror)
|
|
} else {
|
|
if testCase.opts.Fork.Has() {
|
|
assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
|
|
}
|
|
|
|
if testCase.opts.Mirror.Has() {
|
|
assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
|
|
}
|
|
}
|
|
|
|
if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
|
|
if testCase.opts.Collaborate.Has() {
|
|
if testCase.opts.Collaborate.Value() {
|
|
assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
|
|
} else {
|
|
assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCountRepository(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
testCases := getTestCases()
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
count, err := repo_model.CountRepository(t.Context(), testCase.opts)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(testCase.count), count)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearchRepositoryByTopicName(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
testCases := []struct {
|
|
name string
|
|
opts repo_model.SearchRepoOptions
|
|
count int
|
|
}{
|
|
{
|
|
name: "AllPublic/SearchPublicRepositoriesFromTopicAndName",
|
|
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"},
|
|
count: 2,
|
|
},
|
|
{
|
|
name: "AllPublic/OnlySearchPublicRepositoriesFromTopic",
|
|
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true},
|
|
count: 1,
|
|
},
|
|
{
|
|
name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic",
|
|
opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true},
|
|
count: 2,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
_, count, err := repo_model.SearchRepositoryByName(t.Context(), testCase.opts)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(testCase.count), count)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
// user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit)
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
|
|
const orgID = 3
|
|
|
|
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed")
|
|
|
|
publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true))
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions")
|
|
assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed")
|
|
}
|
|
|
|
// TestUserOrgUnitRepoCondTeamAuthorize pins the team.authorize behavior of userOrgTeamUnitRepoBuilder
|
|
// (exercised through UserOrgUnitRepoCond): an admin/owner team grants every unit even without an explicit
|
|
// team_unit row, while a non-admin team only grants a unit it has an explicit row for. This guards both
|
|
// directions — hiding repos from admin-team members, and over-broadening a plain team's access.
|
|
func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
accessibleRepoIDs := func(userID, orgID int64, unitType unit.Type) []int64 {
|
|
ids, err := repo_model.SearchRepositoryIDsByCondition(t.Context(),
|
|
repo_model.UserOrgUnitRepoCond("`repository`.id", userID, orgID, unitType))
|
|
require.NoError(t, err)
|
|
return ids
|
|
}
|
|
|
|
// Case A: user18 is only on org17's owner team (team5, authorize=owner), linked to the private repo24
|
|
// but with no Actions team_unit row. The owner authorize must still grant it, mirroring the runtime
|
|
// HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
|
|
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24),
|
|
"an owner team grants a unit it has no explicit team_unit row for")
|
|
|
|
// Cases B and C share one subject so the team_unit row is the only difference: user4 is only on org3's
|
|
// write team (team2, authorize=write, non-admin), linked to the private repo3. team2 has an explicit
|
|
// Projects row but none for Actions.
|
|
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3),
|
|
"a non-admin team grants a unit it has an explicit team_unit row for")
|
|
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3),
|
|
"a non-admin team must NOT grant a unit it has no team_unit row for")
|
|
}
|