Reimplement GetUserOrgsList to make it simple and clear (#32486)

Reimplement GetUserOrgsList and also move some functions and test to
org_list file.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
Lunny Xiao 2024-11-13 21:31:47 -08:00 committed by GitHub
parent 3f9c3e7bc3
commit b4abb6deff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 205 additions and 172 deletions

View File

@ -1,78 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"xorm.io/builder"
)
// MinimalOrg represents a simple organization with only the needed columns
type MinimalOrg = Organization
// GetUserOrgsList returns all organizations the given user has access to
func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) {
schema, err := db.TableInfo(new(user_model.User))
if err != nil {
return nil, err
}
outputCols := []string{
"id",
"name",
"full_name",
"visibility",
"avatar",
"avatar_email",
"use_custom_avatar",
}
groupByCols := &strings.Builder{}
for _, col := range outputCols {
fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col)
}
groupByStr := groupByCols.String()
groupByStr = groupByStr[0 : len(groupByStr)-1]
sess := db.GetEngine(ctx)
sess = sess.Select(groupByStr+", count(distinct repo_id) as org_count").
Table("user").
Join("INNER", "team", "`team`.org_id = `user`.id").
Join("INNER", "team_user", "`team`.id = `team_user`.team_id").
Join("LEFT", builder.
Select("id as repo_id, owner_id as repo_owner_id").
From("repository").
Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)), "`repository`.repo_owner_id = `team`.org_id").
Where("`team_user`.uid = ?", user.ID).
GroupBy(groupByStr)
type OrgCount struct {
Organization `xorm:"extends"`
OrgCount int
}
orgCounts := make([]*OrgCount, 0, 10)
if err := sess.
Asc("`user`.name").
Find(&orgCounts); err != nil {
return nil, err
}
orgs := make([]*MinimalOrg, len(orgCounts))
for i, orgCount := range orgCounts {
orgCount.Organization.NumRepos = orgCount.OrgCount
orgs[i] = &orgCount.Organization
}
return orgs, nil
}

View File

@ -25,13 +25,6 @@ import (
"xorm.io/xorm" "xorm.io/xorm"
) )
// ________ .__ __ .__
// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \
// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \
// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| /
// \/ /_____/ \/ \/ \/ \/ \/
// ErrOrgNotExist represents a "OrgNotExist" kind of error. // ErrOrgNotExist represents a "OrgNotExist" kind of error.
type ErrOrgNotExist struct { type ErrOrgNotExist struct {
ID int64 ID int64
@ -465,42 +458,6 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
And("team_user.org_id = ?", orgID).Find(&users) And("team_user.org_id = ?", orgID).Find(&users)
} }
// SearchOrganizationsOptions options to filter organizations
type SearchOrganizationsOptions struct {
db.ListOptions
All bool
}
// FindOrgOptions finds orgs options
type FindOrgOptions struct {
db.ListOptions
UserID int64
IncludePrivate bool
}
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
cond := builder.Eq{"uid": userID}
if !includePrivate {
cond["is_public"] = true
}
return builder.Select("org_id").From("org_user").Where(cond)
}
func (opts FindOrgOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
if opts.UserID > 0 {
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate)))
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
}
return cond
}
func (opts FindOrgOptions) ToOrders() string {
return "`user`.name ASC"
}
// HasOrgOrUserVisible tells if the given user can see the given org or user // 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 HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
// If user is nil, it's an anonymous user/request. // If user is nil, it's an anonymous user/request.
@ -533,20 +490,6 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.
return false return false
} }
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
// are allowed to create repos.
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
orgs := make([]*Organization, 0, 10)
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
Where(builder.Eq{"`team_user`.uid": userID}).
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
Asc("`user`.name").
Find(&orgs)
}
// GetOrgUsersByOrgID returns all organization-user relations by organization ID. // GetOrgUsersByOrgID returns all organization-user relations by organization ID.
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) { func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID) sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)

View File

@ -0,0 +1,138 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"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"
"xorm.io/builder"
)
// SearchOrganizationsOptions options to filter organizations
type SearchOrganizationsOptions struct {
db.ListOptions
All bool
}
// FindOrgOptions finds orgs options
type FindOrgOptions struct {
db.ListOptions
UserID int64
IncludePrivate bool
}
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
cond := builder.Eq{"uid": userID}
if !includePrivate {
cond["is_public"] = true
}
return builder.Select("org_id").From("org_user").Where(cond)
}
func (opts FindOrgOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
if opts.UserID > 0 {
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate)))
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
}
return cond
}
func (opts FindOrgOptions) ToOrders() string {
return "`user`.lower_name ASC"
}
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
// are allowed to create repos.
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
orgs := make([]*Organization, 0, 10)
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
Where(builder.Eq{"`team_user`.uid": userID}).
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
Asc("`user`.name").
Find(&orgs)
}
// MinimalOrg represents a simple organization with only the needed columns
type MinimalOrg = Organization
// GetUserOrgsList returns all organizations the given user has access to
func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) {
schema, err := db.TableInfo(new(user_model.User))
if err != nil {
return nil, err
}
outputCols := []string{
"id",
"name",
"full_name",
"visibility",
"avatar",
"avatar_email",
"use_custom_avatar",
}
selectColumns := &strings.Builder{}
for i, col := range outputCols {
fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col)
if i < len(outputCols)-1 {
selectColumns.WriteString(", ")
}
}
columnsStr := selectColumns.String()
var orgs []*MinimalOrg
if err := db.GetEngine(ctx).Select(columnsStr).
Table("user").
Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))).
Find(&orgs); err != nil {
return nil, err
}
type orgCount struct {
OrgID int64
RepoCount int
}
var orgCounts []orgCount
if err := db.GetEngine(ctx).
Select("owner_id AS org_id, COUNT(DISTINCT(repository.id)) as repo_count").
Table("repository").
Join("INNER", "org_user", "owner_id = org_user.org_id").
Where("org_user.uid = ?", user.ID).
And(builder.Or(
builder.Eq{"repository.is_private": false},
builder.In("repository.id", builder.Select("repo_id").From("team_repo").
InnerJoin("team_user", "team_user.team_id = team_repo.team_id").
Where(builder.Eq{"team_user.uid": user.ID})),
builder.In("repository.id", builder.Select("repo_id").From("collaboration").
Where(builder.Eq{"user_id": user.ID})),
)).
GroupBy("owner_id").Find(&orgCounts); err != nil {
return nil, err
}
orgCountMap := make(map[int64]int, len(orgCounts))
for _, orgCount := range orgCounts {
orgCountMap[orgCount.OrgID] = orgCount.RepoCount
}
for _, org := range orgs {
org.NumRepos = orgCountMap[org.ID]
}
return orgs, nil
}

View File

@ -0,0 +1,62 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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"
"github.com/stretchr/testify/assert"
)
func TestCountOrganizations(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
assert.NoError(t, err)
cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true})
assert.NoError(t, err)
assert.Equal(t, expected, cnt)
}
func TestFindOrgs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: true,
})
assert.NoError(t, err)
if assert.Len(t, orgs, 1) {
assert.EqualValues(t, 3, orgs[0].ID)
}
orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: false,
})
assert.NoError(t, err)
assert.Len(t, orgs, 0)
total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: true,
})
assert.NoError(t, err)
assert.EqualValues(t, 1, total)
}
func TestGetUserOrgsList(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
assert.NoError(t, err)
if assert.Len(t, orgs, 1) {
assert.EqualValues(t, 3, orgs[0].ID)
// repo_id: 3 is in the team, 32 is public, 5 is private with no team
assert.EqualValues(t, 2, orgs[0].NumRepos)
}
}

View File

@ -129,15 +129,6 @@ func TestGetOrgByName(t *testing.T) {
assert.True(t, organization.IsErrOrgNotExist(err)) assert.True(t, organization.IsErrOrgNotExist(err))
} }
func TestCountOrganizations(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
assert.NoError(t, err)
cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true})
assert.NoError(t, err)
assert.Equal(t, expected, cnt)
}
func TestIsOrganizationOwner(t *testing.T) { func TestIsOrganizationOwner(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
test := func(orgID, userID int64, expected bool) { test := func(orgID, userID int64, expected bool) {
@ -251,33 +242,6 @@ func TestRestrictedUserOrgMembers(t *testing.T) {
} }
} }
func TestFindOrgs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: true,
})
assert.NoError(t, err)
if assert.Len(t, orgs, 1) {
assert.EqualValues(t, 3, orgs[0].ID)
}
orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: false,
})
assert.NoError(t, err)
assert.Len(t, orgs, 0)
total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
UserID: 4,
IncludePrivate: true,
})
assert.NoError(t, err)
assert.EqualValues(t, 1, total)
}
func TestGetOrgUsersByOrgID(t *testing.T) { func TestGetOrgUsersByOrgID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
auth "code.gitea.io/gitea/models/auth" auth "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization" org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -192,7 +193,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
// returns a list of "org" and "org:team" strings, // returns a list of "org" and "org:team" strings,
// that the given user is a part of. // that the given user is a part of.
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) { func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
orgs, err := org_model.GetUserOrgsList(ctx, user) orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
UserID: user.ID,
IncludePrivate: true,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("GetUserOrgList: %w", err) return nil, fmt.Errorf("GetUserOrgList: %w", err)
} }