Skip to content

Commit

Permalink
Limit org member view of restricted users (#32211)
Browse files Browse the repository at this point in the history
currently restricted users can only see the repos of teams in orgs they
are part at.
they also should only see the users that are also part at the same team.


---
*Sponsored by Kithara Software GmbH*
  • Loading branch information
6543 authored Nov 12, 2024
1 parent 2763766 commit 4c924bf
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 3 deletions.
6 changes: 6 additions & 0 deletions models/fixtures/org_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,9 @@
uid: 2
org_id: 35
is_public: true

-
id: 23
uid: 20
org_id: 17
is_public: false
2 changes: 1 addition & 1 deletion models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@
num_stars: 0
num_repos: 2
num_teams: 3
num_members: 4
num_members: 5
visibility: 0
repo_admin_change_team_access: false
theme: ""
Expand Down
33 changes: 31 additions & 2 deletions models/organization/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/util"

"xorm.io/builder"
"xorm.io/xorm"
)

// ________ .__ __ .__
Expand Down Expand Up @@ -205,11 +206,28 @@ func (opts FindOrgMembersOpts) PublicOnly() bool {
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
}

// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess *xorm.Session) {
if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
teamMates := builder.Select("DISTINCT team_user.uid").
From("team_user").
Where(builder.In("team_user.team_id", getUserTeamIDsQueryBuilder(opts.OrgID, opts.Doer.ID))).
And(builder.Eq{"team_user.org_id": opts.OrgID})

sess.And(
builder.In("org_user.uid", teamMates).
Or(builder.Eq{"org_user.is_public": true}),
)
}
}

// CountOrgMembers counts the organization's members
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
if opts.PublicOnly() {
sess.And("is_public = ?", true)
sess = sess.And("is_public = ?", true)
} else {
opts.applyTeamMatesOnlyFilter(sess)
}

return sess.Count(new(OrgUser))
Expand Down Expand Up @@ -533,7 +551,9 @@ func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organiz
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
if opts.PublicOnly() {
sess.And("is_public = ?", true)
sess = sess.And("is_public = ?", true)
} else {
opts.applyTeamMatesOnlyFilter(sess)
}

if opts.ListOptions.PageSize > 0 {
Expand Down Expand Up @@ -664,6 +684,15 @@ func (org *Organization) getUserTeamIDs(ctx context.Context, userID int64) ([]in
Find(&teamIDs)
}

func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
return builder.Select("team.id").From("team").
InnerJoin("team_user", "team_user.team_id = team.id").
Where(builder.Eq{
"team_user.org_id": orgID,
"team_user.uid": userID,
})
}

// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
Expand Down
70 changes: 70 additions & 0 deletions models/organization/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package organization_test

import (
"slices"
"sort"
"testing"

Expand Down Expand Up @@ -181,6 +182,75 @@ func TestIsPublicMembership(t *testing.T) {
test(unittest.NonexistentID, unittest.NonexistentID, false)
}

func TestRestrictedUserOrgMembers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{
ID: 29,
IsRestricted: true,
})
if !assert.True(t, restrictedUser.IsRestricted) {
return // ensure fixtures return restricted user
}

testCases := []struct {
name string
opts *organization.FindOrgMembersOpts
expectedUIDs []int64
}{
{
name: "restricted user sees public members and teammates",
opts: &organization.FindOrgMembersOpts{
OrgID: 17, // org17 where user29 is in team9
Doer: restrictedUser,
IsDoerMember: true,
},
expectedUIDs: []int64{2, 15, 20, 29}, // Public members (2) + teammates in team9 (15, 20, 29)
},
{
name: "restricted user sees only public members when not member",
opts: &organization.FindOrgMembersOpts{
OrgID: 3, // org3 where user29 is not a member
Doer: restrictedUser,
},
expectedUIDs: []int64{2, 28}, // Only public members
},
{
name: "non logged in only shows public members",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
},
expectedUIDs: []int64{2, 28}, // Only public members
},
{
name: "non restricted user sees all members",
opts: &organization.FindOrgMembersOpts{
OrgID: 17,
Doer: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}),
IsDoerMember: true,
},
expectedUIDs: []int64{2, 15, 18, 20, 29}, // All members
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
count, err := organization.CountOrgMembers(db.DefaultContext, tc.opts)
assert.NoError(t, err)
assert.EqualValues(t, len(tc.expectedUIDs), count)

members, err := organization.GetOrgUsersByOrgID(db.DefaultContext, 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.EqualValues(t, tc.expectedUIDs, memberUIDs)
})
}
}

func TestFindOrgs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

Expand Down

0 comments on commit 4c924bf

Please sign in to comment.