This commit is contained in:
@@ -6,14 +6,14 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
_ "code.gitea.io/gitea/models/activities"
|
||||
_ "code.gitea.io/gitea/models/organization"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
_ "code.gitea.io/gitea/models/user"
|
||||
_ "gitea.dev/models"
|
||||
_ "gitea.dev/models/actions"
|
||||
_ "gitea.dev/models/activities"
|
||||
_ "gitea.dev/models/organization"
|
||||
_ "gitea.dev/models/repo"
|
||||
_ "gitea.dev/models/user"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -9,17 +9,16 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
|
||||
@@ -92,6 +91,14 @@ func (org *Organization) IsOwnedBy(ctx context.Context, uid int64) (bool, error)
|
||||
return IsOrganizationOwner(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// CanChangeRepoTeamAccess reports whether a repository administrator can change team access.
|
||||
func (org *Organization) CanChangeRepoTeamAccess(ctx context.Context, doer *user_model.User) (bool, error) {
|
||||
if org.RepoAdminChangeTeamAccess || doer.IsAdmin {
|
||||
return true, nil
|
||||
}
|
||||
return org.IsOwnedBy(ctx, doer.ID)
|
||||
}
|
||||
|
||||
// IsOrgAdmin returns true if given user is in the owner team or an admin team.
|
||||
func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error) {
|
||||
return IsOrganizationAdmin(ctx, org.ID, uid)
|
||||
@@ -184,14 +191,44 @@ type FindOrgMembersOpts struct {
|
||||
Doer *user_model.User
|
||||
IsDoerMember bool
|
||||
OrgID int64
|
||||
Keyword string
|
||||
}
|
||||
|
||||
func (opts FindOrgMembersOpts) PublicOnly() bool {
|
||||
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
|
||||
}
|
||||
|
||||
// applyKeywordFilter adds keyword search conditions to session
|
||||
func (opts FindOrgMembersOpts) applyKeywordFilter(sess db.Session) bool {
|
||||
if opts.Keyword == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
keywordCond := builder.Or(
|
||||
db.BuildCaseInsensitiveLike("`user`.lower_name", opts.Keyword),
|
||||
db.BuildCaseInsensitiveLike("`user`.full_name", opts.Keyword),
|
||||
)
|
||||
|
||||
emailCond := db.BuildCaseInsensitiveLike("`user`.email", opts.Keyword)
|
||||
switch {
|
||||
case opts.Doer == nil:
|
||||
emailCond = emailCond.And(builder.Eq{"`user`.keep_email_private": false})
|
||||
case !opts.Doer.IsAdmin:
|
||||
emailCond = emailCond.And(
|
||||
builder.Or(
|
||||
builder.Eq{"`user`.keep_email_private": false},
|
||||
builder.Eq{"`user`.id": opts.Doer.ID},
|
||||
),
|
||||
)
|
||||
}
|
||||
keywordCond = keywordCond.Or(emailCond)
|
||||
|
||||
_ = sess.Join("INNER", "`user`", "org_user.uid = `user`.id").And(keywordCond)
|
||||
return true
|
||||
}
|
||||
|
||||
// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
|
||||
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess *xorm.Session) {
|
||||
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess db.Session) {
|
||||
if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
|
||||
teamMates := builder.Select("DISTINCT team_user.uid").
|
||||
From("team_user").
|
||||
@@ -213,6 +250,7 @@ func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, erro
|
||||
} else {
|
||||
opts.applyTeamMatesOnlyFilter(sess)
|
||||
}
|
||||
_ = opts.applyKeywordFilter(sess)
|
||||
|
||||
return sess.Count(new(OrgUser))
|
||||
}
|
||||
@@ -273,7 +311,7 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us
|
||||
}
|
||||
}
|
||||
|
||||
if org.Visibility.IsPublic() {
|
||||
if ownerVisibilitySatisfiesDoer(org.AsUser(), doer) {
|
||||
return perm.AccessModeRead
|
||||
}
|
||||
|
||||
@@ -340,6 +378,7 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
|
||||
NumMembers: 1,
|
||||
IncludesAllRepositories: true,
|
||||
CanCreateOrgRepo: true,
|
||||
Visibility: structs.VisibleTypeLimited,
|
||||
}
|
||||
if err = db.Insert(ctx, t); err != nil {
|
||||
return fmt.Errorf("insert owner team: %w", err)
|
||||
@@ -380,11 +419,8 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrOrgNotExist{0, name}
|
||||
}
|
||||
u := &Organization{
|
||||
LowerName: strings.ToLower(name),
|
||||
Type: user_model.UserTypeOrganization,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
|
||||
u, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -417,8 +453,7 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
|
||||
And("team_user.org_id = ?", orgID).Find(&users)
|
||||
}
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
||||
func ownerVisibilitySatisfiesDoer(orgOrUser, user *user_model.User) bool {
|
||||
// If user is nil, it's an anonymous user/request.
|
||||
// The Ghost user is handled like an anonymous user.
|
||||
if user == nil || user.IsGhost() {
|
||||
@@ -433,10 +468,13 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User)
|
||||
return true
|
||||
}
|
||||
|
||||
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return orgOrUser.Visibility != structs.VisibleTypePrivate && !user.IsRestricted
|
||||
}
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool {
|
||||
return ownerVisibilitySatisfiesDoer(owner, doer) ||
|
||||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))
|
||||
}
|
||||
|
||||
// HasOrgsVisible tells if the given user can see at least one of the orgs provided
|
||||
@@ -461,9 +499,13 @@ func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUs
|
||||
} else {
|
||||
opts.applyTeamMatesOnlyFilter(sess)
|
||||
}
|
||||
if opts.applyKeywordFilter(sess) {
|
||||
sess = sess.Select("org_user.*")
|
||||
}
|
||||
|
||||
sess = sess.OrderBy("org_user.uid ASC")
|
||||
if opts.ListOptions.PageSize > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
db.SetSessionPagination(sess, opts)
|
||||
|
||||
ous := make([]*OrgUser, 0, opts.PageSize)
|
||||
return ous, sess.Find(&ous)
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -89,6 +89,9 @@ func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType {
|
||||
if doer.IsAdmin || doer.ID == other.ID {
|
||||
return structs.VisibleTypePrivate
|
||||
}
|
||||
if doer.IsRestricted {
|
||||
return structs.VisibleTypePublic
|
||||
}
|
||||
return structs.VisibleTypeLimited
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"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"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -77,8 +77,14 @@ func testLoadOrgListTeams(t *testing.T) {
|
||||
}
|
||||
|
||||
func testDoerViewOtherVisibility(t *testing.T) {
|
||||
viewer := &user_model.User{ID: 1}
|
||||
other := &user_model.User{ID: 2}
|
||||
restrictedViewer := &user_model.User{ID: 3, IsRestricted: true}
|
||||
|
||||
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil))
|
||||
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2}))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1}))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2}))
|
||||
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(viewer, other))
|
||||
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(restrictedViewer, other))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(viewer, viewer))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(restrictedViewer, restrictedViewer))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 4, IsAdmin: true, IsRestricted: true}, other))
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
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/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -288,6 +290,76 @@ func TestGetOrgUsersByOrgID(t *testing.T) {
|
||||
assert.Empty(t, orgUsers)
|
||||
}
|
||||
|
||||
func TestOrgMembersSearch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts *organization.FindOrgMembersOpts
|
||||
expectedUIDs []int64
|
||||
}{
|
||||
{
|
||||
name: "match by username",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3,
|
||||
Doer: member,
|
||||
IsDoerMember: true,
|
||||
Keyword: "user4",
|
||||
},
|
||||
expectedUIDs: []int64{4},
|
||||
},
|
||||
{
|
||||
name: "match by full name",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3,
|
||||
Doer: member,
|
||||
IsDoerMember: true,
|
||||
Keyword: "user27",
|
||||
},
|
||||
expectedUIDs: []int64{28},
|
||||
},
|
||||
{
|
||||
name: "private email hidden",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3,
|
||||
Doer: member,
|
||||
IsDoerMember: true,
|
||||
Keyword: "user2@example.com",
|
||||
},
|
||||
expectedUIDs: []int64{},
|
||||
},
|
||||
{
|
||||
name: "admin can search private email",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3,
|
||||
Doer: admin,
|
||||
Keyword: "user2@example.com",
|
||||
},
|
||||
expectedUIDs: []int64{2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
count, err := organization.CountOrgMembers(t.Context(), tc.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, len(tc.expectedUIDs), count)
|
||||
|
||||
members, err := organization.GetOrgUsersByOrgID(t.Context(), tc.opts)
|
||||
assert.NoError(t, err)
|
||||
memberUIDs := make([]int64, 0, len(members))
|
||||
for _, member := range members {
|
||||
memberUIDs = append(memberUIDs, member.UID)
|
||||
}
|
||||
slices.Sort(memberUIDs)
|
||||
assert.Equal(t, tc.expectedUIDs, memberUIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeOrgUserStatus(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -456,6 +528,22 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) {
|
||||
assert.NotNil(t, users[5])
|
||||
}
|
||||
|
||||
func TestCanCreateOrgRepoByOwnerTeamWithoutFlag(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
ownerTeam, err := org.GetOwnerTeam(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
ownerTeam.CanCreateOrgRepo = false
|
||||
_, err = db.GetEngine(t.Context()).ID(ownerTeam.ID).Cols("can_create_org_repo").Update(ownerTeam)
|
||||
require.NoError(t, err)
|
||||
|
||||
ok, err := organization.CanCreateOrgRepo(t.Context(), org.ID, 2)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestUser_RemoveOrgRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
@@ -540,3 +628,10 @@ func TestCreateOrganization4(t *testing.T) {
|
||||
assert.True(t, db.IsErrNameReserved(err))
|
||||
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestOrAnyRepoUnitPermission(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
org := organization.Organization{Visibility: structs.VisibleTypeLimited}
|
||||
assert.Equal(t, perm.AccessModeNone, org.UnitPermission(t.Context(), nil, unit.TypeWiki))
|
||||
assert.Equal(t, perm.AccessModeRead, org.UnitPermission(t.Context(), &user_model.User{}, unit.TypeWiki))
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -104,7 +105,7 @@ func IsPublicMembership(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
// CanCreateOrgRepo returns true if user can create repo in organization
|
||||
func CanCreateOrgRepo(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where(builder.Eq{"team.can_create_org_repo": true}).
|
||||
Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
And("team_user.uid = ?", uid).
|
||||
And("team_user.org_id = ?", orgID).
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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/setting"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -6,15 +6,17 @@ package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -81,9 +83,45 @@ type Team struct {
|
||||
Members []*user_model.User `xorm:"-"`
|
||||
NumRepos int
|
||||
NumMembers int
|
||||
Units []*TeamUnit `xorm:"-"`
|
||||
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Units []*TeamUnit `xorm:"-"`
|
||||
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"`
|
||||
}
|
||||
|
||||
func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
|
||||
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
|
||||
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
|
||||
|
||||
const (
|
||||
ghostTeamID = -1
|
||||
ghostTeamName = "(deleted team)"
|
||||
)
|
||||
|
||||
func newGhostTeam() *Team {
|
||||
return &Team{ID: ghostTeamID, Name: ghostTeamName, LowerName: ghostTeamName}
|
||||
}
|
||||
|
||||
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
|
||||
// the team's metadata, based on the team's visibility tier and the parent org's
|
||||
// visibility. Privileged callers (site admins, org owners, team members) are
|
||||
// decided by the caller before reaching here.
|
||||
func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.User) (bool, error) {
|
||||
switch t.Visibility {
|
||||
case structs.VisibleTypePublic:
|
||||
return HasOrgOrUserVisible(ctx, org, doer), nil
|
||||
case structs.VisibleTypeLimited:
|
||||
return IsOrganizationMember(ctx, t.OrgID, doer.ID)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeTeamVisibility(s string) structs.VisibleType {
|
||||
if vt, ok := structs.VisibilityModes[s]; ok {
|
||||
return vt
|
||||
}
|
||||
return structs.VisibleTypePrivate
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -242,6 +280,17 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func GetPossibleTeamByID(ctx context.Context, teamID int64) (int64, *Team, error) {
|
||||
t, err := GetTeamByID(ctx, teamID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
t = newGhostTeam()
|
||||
return t.ID, t, nil
|
||||
} else if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return t.ID, t, nil
|
||||
}
|
||||
|
||||
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
||||
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
||||
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -116,10 +116,7 @@ func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, em
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.CryptoRandomString(25)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := util.CryptoRandomString(25)
|
||||
|
||||
invite := &TeamInvite{
|
||||
Token: token,
|
||||
|
||||
@@ -6,9 +6,9 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -50,9 +52,15 @@ type SearchTeamOptions struct {
|
||||
Keyword string
|
||||
OrgID int64
|
||||
IncludeDesc bool
|
||||
// IncludeVisibilities, when combined with UserID, also returns teams whose
|
||||
// visibility is in this list, even if UserID is not a member. Typical values:
|
||||
// - {limited,public} for org members
|
||||
// - {public} for signed-in users who are not org members
|
||||
// Leave empty to return only teams the user is a member of.
|
||||
IncludeVisibilities []structs.VisibleType
|
||||
}
|
||||
|
||||
func (opts *SearchTeamOptions) toCond() builder.Cond {
|
||||
func (opts *SearchTeamOptions) applyToSession(sess db.SQLSession) {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.Keyword) > 0 {
|
||||
@@ -68,11 +76,51 @@ func (opts *SearchTeamOptions) toCond() builder.Cond {
|
||||
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
|
||||
}
|
||||
|
||||
if opts.UserID > 0 {
|
||||
switch {
|
||||
case opts.UserID > 0 && len(opts.IncludeVisibilities) > 0:
|
||||
sess = sess.Join("LEFT", "team_user", "team_user.team_id = team.id AND team_user.uid = ?", opts.UserID)
|
||||
cond = cond.And(builder.Or(
|
||||
builder.Eq{"team_user.uid": opts.UserID},
|
||||
builder.In("`team`.visibility", opts.IncludeVisibilities),
|
||||
))
|
||||
case opts.UserID > 0:
|
||||
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
|
||||
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
|
||||
case len(opts.IncludeVisibilities) > 0:
|
||||
cond = cond.And(builder.In("`team`.visibility", opts.IncludeVisibilities))
|
||||
}
|
||||
sess.Where(cond)
|
||||
}
|
||||
|
||||
return cond
|
||||
func VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn bool) []structs.VisibleType {
|
||||
switch {
|
||||
case isOrgMember:
|
||||
return []structs.VisibleType{structs.VisibleTypeLimited, structs.VisibleTypePublic}
|
||||
case isSignedIn:
|
||||
return []structs.VisibleType{structs.VisibleTypePublic}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyTeamListFilter(ctx context.Context, orgID int64, viewer *user_model.User, isSignedIn bool, opts *SearchTeamOptions) error {
|
||||
if viewer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
isOwner, err := IsOrganizationOwner(ctx, orgID, viewer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOwner {
|
||||
return nil
|
||||
}
|
||||
isOrgMember, err := IsOrganizationMember(ctx, orgID, viewer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.UserID = viewer.ID
|
||||
opts.IncludeVisibilities = VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchTeam search for teams. Caller is responsible to check permissions.
|
||||
@@ -80,15 +128,12 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64,
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
opts.SetDefaultValues()
|
||||
cond := opts.toCond()
|
||||
opts.applyToSession(sess)
|
||||
|
||||
if opts.UserID > 0 {
|
||||
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
|
||||
}
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
db.SetSessionPagination(sess, opts)
|
||||
|
||||
teams := make([]*Team, 0, opts.PageSize)
|
||||
count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
|
||||
count, err := sess.OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
org_model "gitea.dev/models/organization"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,9 +6,9 @@ package organization
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -6,11 +6,11 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,10 +6,12 @@ package organization_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -38,6 +40,43 @@ func TestTeam_IsMember(t *testing.T) {
|
||||
assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID))
|
||||
}
|
||||
|
||||
func TestTeam_CanNonMemberReadMeta(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // public org
|
||||
org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 35}) // private org
|
||||
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // member of org 3 and org 35
|
||||
outsider := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // member of neither org
|
||||
|
||||
test := func(name string, team *organization.Team, org, doer *user_model.User, expected bool) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ok, err := team.CanNonMemberReadMeta(t.Context(), org, doer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// Public team is gated only by the parent org's visibility.
|
||||
publicTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePublic}
|
||||
test("public team, public org, member", publicTeam, org3, member, true)
|
||||
test("public team, public org, outsider", publicTeam, org3, outsider, true)
|
||||
|
||||
// Public team inside a private org: only org members may see it.
|
||||
publicTeamPrivOrg := &organization.Team{OrgID: 35, Visibility: structs.VisibleTypePublic}
|
||||
test("public team, private org, org member", publicTeamPrivOrg, org35, member, true)
|
||||
test("public team, private org, outsider", publicTeamPrivOrg, org35, outsider, false)
|
||||
|
||||
// Limited team: any org member, but never outsiders.
|
||||
limitedTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypeLimited}
|
||||
test("limited team, org member", limitedTeam, org3, member, true)
|
||||
test("limited team, outsider", limitedTeam, org3, outsider, false)
|
||||
|
||||
// Private team is never visible to non-members; members/owners are admitted by the caller.
|
||||
privateTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePrivate}
|
||||
test("private team, org member", privateTeam, org3, member, false)
|
||||
test("private team, outsider", privateTeam, org3, outsider, false)
|
||||
}
|
||||
|
||||
func TestTeam_GetRepositories(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -172,6 +211,52 @@ func TestGetUserOrgTeams(t *testing.T) {
|
||||
test(3, unittest.NonexistentID)
|
||||
}
|
||||
|
||||
func TestSearchTeamIncludeVisible(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const orgID int64 = 3
|
||||
// User 5 is an org member but only belongs to team 1 (Owners) — make sure
|
||||
// they don't see team 2 (default private) but do see a freshly added
|
||||
// limited team they are not a member of.
|
||||
visible := &organization.Team{
|
||||
OrgID: orgID,
|
||||
LowerName: "visible-team",
|
||||
Name: "visible-team",
|
||||
AccessMode: 1, // read
|
||||
Visibility: structs.VisibleTypeLimited,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), visible))
|
||||
teams, _, err := organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
|
||||
OrgID: orgID,
|
||||
UserID: 2,
|
||||
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
ids := make(map[int64]bool, len(teams))
|
||||
for _, team := range teams {
|
||||
assert.Equal(t, orgID, team.OrgID)
|
||||
ids[team.ID] = true
|
||||
}
|
||||
// user 2 is in team 1 and team 2 in org 3, plus should see the new visible team.
|
||||
assert.True(t, ids[1], "expected to see team 1 (member)")
|
||||
assert.True(t, ids[2], "expected to see team 2 (member)")
|
||||
assert.True(t, ids[visible.ID], "expected to see visible team")
|
||||
|
||||
// user 5 is only an org member in team 1, must not see secret team 2 but must see the visible one.
|
||||
teams, _, err = organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
|
||||
OrgID: orgID,
|
||||
UserID: 5,
|
||||
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
ids = make(map[int64]bool, len(teams))
|
||||
for _, team := range teams {
|
||||
ids[team.ID] = true
|
||||
}
|
||||
assert.False(t, ids[2], "user 5 must not see private team 2")
|
||||
assert.True(t, ids[visible.ID], "user 5 must see the limited team")
|
||||
}
|
||||
|
||||
func TestHasTeamRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ package organization
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
)
|
||||
|
||||
// TeamUnit describes all units of a repository
|
||||
@@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit {
|
||||
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
|
||||
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
|
||||
}
|
||||
|
||||
// UpdateTeamUnits updates a teams's units
|
||||
func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(units) > 0 {
|
||||
if err = db.Insert(ctx, units); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ package organization
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -36,14 +36,6 @@ type SearchMembersOptions struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
func (opts SearchMembersOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.TeamID > 0 {
|
||||
cond = cond.And(builder.Eq{"": opts.TeamID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// GetTeamMembers returns all members in given team of organization.
|
||||
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
|
||||
var members []*user_model.User
|
||||
|
||||
Reference in New Issue
Block a user