From 10fd9e9e38555b6caecb7d37ba1638f2b4262c9c Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 24 Aug 2023 01:00:45 -0400 Subject: [PATCH 01/29] db: migrate `org.go` to `orgs.go` with GORM --- .github/workflows/go.yml | 10 +- docs/dev/database_schema.md | 19 + internal/context/org.go | 4 +- internal/db/access_tokens.go | 2 +- internal/db/actions.go | 23 +- internal/db/backup_test.go | 19 +- internal/db/db.go | 1 + internal/db/error.go | 20 - internal/db/lfs.go | 3 +- internal/db/login_sources.go | 2 +- internal/db/migrations/migrations.go | 2 +- internal/db/models.go | 4 +- internal/db/org.go | 373 +---------------- internal/db/org_team.go | 143 +++---- internal/db/orgs.go | 390 +++++++++++++++++- internal/db/repo.go | 49 ++- internal/db/repos.go | 7 +- .../db/testdata/backup/OrgUser.golden.json | 2 + internal/db/two_factors.go | 2 +- internal/db/users.go | 49 ++- internal/db/users_test.go | 23 +- internal/route/admin/orgs.go | 6 +- internal/route/api/v1/org/org.go | 2 +- internal/route/api/v1/repo/repo.go | 11 +- internal/route/home.go | 6 +- internal/route/org/members.go | 17 +- internal/route/user/home.go | 34 +- internal/route/user/setting.go | 2 +- 28 files changed, 663 insertions(+), 562 deletions(-) create mode 100644 internal/db/testdata/backup/OrgUser.golden.json diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9feee4e24f..dae14e83721 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -61,7 +61,7 @@ jobs: name: Test strategy: matrix: - go-version: [ 1.20.x, 1.21.x ] + go-version: [ 1.21.x ] platform: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.platform }} steps: @@ -101,7 +101,7 @@ jobs: name: Test Windows strategy: matrix: - go-version: [ 1.20.x, 1.21.x ] + go-version: [ 1.21.x ] platform: [ windows-latest ] runs-on: ${{ matrix.platform }} steps: @@ -139,7 +139,7 @@ jobs: name: Postgres strategy: matrix: - go-version: [ 1.20.x, 1.21.x ] + go-version: [ 1.21.x ] platform: [ ubuntu-latest ] runs-on: ${{ matrix.platform }} services: @@ -175,7 +175,7 @@ jobs: name: MySQL strategy: matrix: - go-version: [ 1.20.x, 1.21.x ] + go-version: [ 1.21.x ] platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} steps: @@ -200,7 +200,7 @@ jobs: name: SQLite - Go strategy: matrix: - go-version: [ 1.20.x, 1.21.x ] + go-version: [ 1.21.x ] platform: [ ubuntu-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/docs/dev/database_schema.md b/docs/dev/database_schema.md index b49cca3e611..613f1831ebe 100644 --- a/docs/dev/database_schema.md +++ b/docs/dev/database_schema.md @@ -129,3 +129,22 @@ Primary keys: id Primary keys: id ``` +# Table "org_user" + +``` + FIELD | COLUMN | POSTGRESQL | MYSQL | SQLITE3 +-----------+-----------+--------------------------------+--------------------------------+--------------------------------- + ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER + UserID | uid | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL + OrgID | org_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL + IsPublic | is_public | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE + IsOwner | is_owner | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE + NumTeams | num_teams | BIGINT NOT NULL DEFAULT 0 | BIGINT NOT NULL DEFAULT 0 | INTEGER NOT NULL DEFAULT 0 + +Primary keys: id +Indexes: + "idx_org_user_org_id" (org_id) + "idx_org_user_user_id" (uid) + "org_user_user_org_unique" UNIQUE (uid, org_id) +``` + diff --git a/internal/context/org.go b/internal/context/org.go index 389677af915..51256358a00 100644 --- a/internal/context/org.go +++ b/internal/context/org.go @@ -73,8 +73,8 @@ func HandleOrgAssignment(c *Context, args ...bool) { c.Org.IsMember = true c.Org.IsTeamMember = true c.Org.IsTeamAdmin = true - } else if org.IsOrgMember(c.User.ID) { - c.Org.IsMember = true + } else { + c.Org.IsMember, _ = db.Orgs.HasMember(c.Req.Context(), org.ID, c.User.ID) } } else { // Fake data. diff --git a/internal/db/access_tokens.go b/internal/db/access_tokens.go index 825cfa87a04..f9b63e15462 100644 --- a/internal/db/access_tokens.go +++ b/internal/db/access_tokens.go @@ -154,7 +154,7 @@ func (db *accessTokens) GetBySHA1(ctx context.Context, sha1 string) (*AccessToke token := new(AccessToken) err := db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} } return nil, err diff --git a/internal/db/actions.go b/internal/db/actions.go index 48d080b3d7e..a67cb0b8d1e 100644 --- a/internal/db/actions.go +++ b/internal/db/actions.go @@ -86,19 +86,20 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after /* Equivalent SQL for PostgreSQL: - SELECT * FROM "action" + WHERE user_id = @userID AND (@skipAfter OR id < @afterID) @@ -153,8 +154,8 @@ func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int6 Where("?", !isProfile || actorID == userID). Or("is_private = ? AND act_user_id = ?", false, userID), ). - Limit(conf.UI.User.NewsFeedPagingNum). - Order("id DESC") + Order("id DESC"). + Limit(conf.UI.User.NewsFeedPagingNum) } func (db *actions) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) { diff --git a/internal/db/backup_test.go b/internal/db/backup_test.go index 9d2d9403a40..391af791a7a 100644 --- a/internal/db/backup_test.go +++ b/internal/db/backup_test.go @@ -31,7 +31,7 @@ func TestDumpAndImport(t *testing.T) { } t.Parallel() - const wantTables = 8 + const wantTables = 9 if len(Tables) != wantTables { t.Fatalf("New table has added (want %d got %d), please add new tests for the table and update this check", wantTables, len(Tables)) } @@ -197,6 +197,23 @@ func setupDBToDump(t *testing.T, db *gorm.DB) { Description: "This is a notice", CreatedUnix: 1588568886, }, + + &OrgUser{ + ID: 1, + UserID: 1, + OrgID: 11, + IsPublic: true, + IsOwner: true, + NumTeams: 3, + }, + &OrgUser{ + ID: 2, + UserID: 2, + OrgID: 11, + IsPublic: false, + IsOwner: false, + NumTeams: 0, + }, } for _, val := range vals { err := db.Create(val).Error diff --git a/internal/db/db.go b/internal/db/db.go index 9878032d3d9..e7dc311199c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -46,6 +46,7 @@ var Tables = []any{ new(Follow), new(LFSObject), new(LoginSource), new(Notice), + new(OrgUser), } // Init initializes the database with given logger. diff --git a/internal/db/error.go b/internal/db/error.go index 8436aa99e08..1c831cf86de 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -133,26 +133,6 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string { return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name) } -// ________ .__ __ .__ -// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____ -// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \ -// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \ -// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| / -// \/ /_____/ \/ \/ \/ \/ \/ - -type ErrLastOrgOwner struct { - UID int64 -} - -func IsErrLastOrgOwner(err error) bool { - _, ok := err.(ErrLastOrgOwner) - return ok -} - -func (err ErrLastOrgOwner) Error() string { - return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID) -} - // __________ .__ __ // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | diff --git a/internal/db/lfs.go b/internal/db/lfs.go index bff18efd15d..6f911661404 100644 --- a/internal/db/lfs.go +++ b/internal/db/lfs.go @@ -9,6 +9,7 @@ import ( "fmt" "time" + "github.com/pkg/errors" "gorm.io/gorm" "gogs.io/gogs/internal/errutil" @@ -75,7 +76,7 @@ func (db *lfs) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID object := new(LFSObject) err := db.WithContext(ctx).Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": oid}} } return nil, err diff --git a/internal/db/login_sources.go b/internal/db/login_sources.go index 9469a3f080e..c5a7b974dfd 100644 --- a/internal/db/login_sources.go +++ b/internal/db/login_sources.go @@ -264,7 +264,7 @@ func (db *loginSources) GetByID(ctx context.Context, id int64) (*LoginSource, er source := new(LoginSource) err := db.WithContext(ctx).Where("id = ?", id).First(source).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return db.files.GetByID(id) } return nil, err diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go index 04c1e36364d..d6cfe6f9404 100644 --- a/internal/db/migrations/migrations.go +++ b/internal/db/migrations/migrations.go @@ -80,7 +80,7 @@ func Migrate(db *gorm.DB) error { var current Version err := db.Where("id = ?", 1).First(¤t).Error - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { err = db.Create( &Version{ ID: 1, diff --git a/internal/db/models.go b/internal/db/models.go index 1e3b8d9b719..ebbe5136d08 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -57,7 +57,7 @@ func init() { new(Label), new(IssueLabel), new(Milestone), new(Mirror), new(Release), new(Webhook), new(HookTask), new(ProtectBranch), new(ProtectBranchWhitelist), - new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), + new(Team), new(TeamUser), new(TeamRepo), ) gonicNames := []string{"SSL"} @@ -211,7 +211,7 @@ type Statistic struct { func GetStatistic(ctx context.Context) (stats Statistic) { stats.Counter.User = Users.Count(ctx) - stats.Counter.Org = CountOrganizations() + stats.Counter.Org = Orgs.Count(ctx) stats.Counter.PublicKey, _ = x.Count(new(PublicKey)) stats.Counter.Repo = CountRepositories(true) stats.Counter.Watch, _ = x.Count(new(Watch)) diff --git a/internal/db/org.go b/internal/db/org.go index 8bc16701e4f..4b780cdc683 100644 --- a/internal/db/org.go +++ b/internal/db/org.go @@ -6,7 +6,6 @@ package db import ( "context" - "errors" "fmt" "os" "strings" @@ -19,87 +18,6 @@ import ( "gogs.io/gogs/internal/userutil" ) -var ErrOrgNotExist = errors.New("Organization does not exist") - -// IsOwnedBy returns true if given user is in the owner team. -func (org *User) IsOwnedBy(userID int64) bool { - return IsOrganizationOwner(org.ID, userID) -} - -// IsOrgMember returns true if given user is member of organization. -func (org *User) IsOrgMember(uid int64) bool { - return org.IsOrganization() && IsOrganizationMember(org.ID, uid) -} - -func (org *User) getTeam(e Engine, name string) (*Team, error) { - return getTeamOfOrgByName(e, org.ID, name) -} - -// GetTeamOfOrgByName returns named team of organization. -func (org *User) GetTeam(name string) (*Team, error) { - return org.getTeam(x, name) -} - -func (org *User) getOwnerTeam(e Engine) (*Team, error) { - return org.getTeam(e, OWNER_TEAM) -} - -// GetOwnerTeam returns owner team of organization. -func (org *User) GetOwnerTeam() (*Team, error) { - return org.getOwnerTeam(x) -} - -func (org *User) getTeams(e Engine) (err error) { - org.Teams, err = getTeamsByOrgID(e, org.ID) - return err -} - -// GetTeams returns all teams that belong to organization. -func (org *User) GetTeams() error { - return org.getTeams(x) -} - -// TeamsHaveAccessToRepo returns all teams that have given access level to the repository. -func (org *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) { - return GetTeamsHaveAccessToRepo(org.ID, repoID, mode) -} - -// GetMembers returns all members of organization. -func (org *User) GetMembers(limit int) error { - ous, err := GetOrgUsersByOrgID(org.ID, limit) - if err != nil { - return err - } - - org.Members = make([]*User, len(ous)) - for i, ou := range ous { - org.Members[i], err = Users.GetByID(context.TODO(), ou.Uid) - if err != nil { - return err - } - } - return nil -} - -// AddMember adds new member to organization. -func (org *User) AddMember(uid int64) error { - return AddOrgUser(org.ID, uid) -} - -// RemoveMember removes member from organization. -func (org *User) RemoveMember(uid int64) error { - return RemoveOrgUser(org.ID, uid) -} - -func (org *User) removeOrgRepo(e Engine, repoID int64) error { - return removeOrgRepo(e, org.ID, repoID) -} - -// RemoveOrgRepo removes all team-repository relations of organization. -func (org *User) RemoveOrgRepo(repoID int64) error { - return org.removeOrgRepo(x, repoID) -} - // CreateOrganization creates record of a new organization. func CreateOrganization(org, owner *User) (err error) { if err = isUsernameAllowed(org.Name); err != nil { @@ -139,7 +57,7 @@ func CreateOrganization(org, owner *User) (err error) { // Add initial creator to organization and owner team. if _, err = sess.Insert(&OrgUser{ - Uid: owner.ID, + UserID: owner.ID, OrgID: org.ID, IsOwner: true, NumTeams: 1, @@ -150,8 +68,8 @@ func CreateOrganization(org, owner *User) (err error) { // Create default owner team. t := &Team{ OrgID: org.ID, - LowerName: strings.ToLower(OWNER_TEAM), - Name: OWNER_TEAM, + LowerName: strings.ToLower(TeamNameOwners), + Name: TeamNameOwners, Authorize: AccessModeOwner, NumMembers: 1, } @@ -174,30 +92,6 @@ func CreateOrganization(org, owner *User) (err error) { return sess.Commit() } -// GetOrgByName returns organization by given name. -func GetOrgByName(name string) (*User, error) { - if name == "" { - return nil, ErrOrgNotExist - } - u := &User{ - LowerName: strings.ToLower(name), - Type: UserTypeOrganization, - } - has, err := x.Get(u) - if err != nil { - return nil, err - } else if !has { - return nil, ErrOrgNotExist - } - return u, nil -} - -// CountOrganizations returns number of organizations. -func CountOrganizations() int64 { - count, _ := x.Where("type=1").Count(new(User)) - return count -} - // Organizations returns number of organizations in given page. func Organizations(page, pageSize int) ([]*User, error) { orgs := make([]*User, 0, pageSize) @@ -237,41 +131,6 @@ func DeleteOrganization(org *User) error { return sess.Commit() } -// ________ ____ ___ -// \_____ \_______ ____ | | \______ ___________ -// / | \_ __ \/ ___\| | / ___// __ \_ __ \ -// / | \ | \/ /_/ > | /\___ \\ ___/| | \/ -// \_______ /__| \___ /|______//____ >\___ >__| -// \/ /_____/ \/ \/ - -// OrgUser represents relations of organizations and their members. -type OrgUser struct { - ID int64 `gorm:"primaryKey"` - Uid int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"` - OrgID int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"` - IsPublic bool `gorm:"not null;default:FALSE"` - IsOwner bool `gorm:"not null;default:FALSE"` - NumTeams int `gorm:"not null;default:0"` -} - -// IsOrganizationOwner returns true if given user is in the owner team. -func IsOrganizationOwner(orgID, userID int64) bool { - has, _ := x.Where("is_owner = ?", true).And("uid = ?", userID).And("org_id = ?", orgID).Get(new(OrgUser)) - return has -} - -// IsOrganizationMember returns true if given user is member of organization. -func IsOrganizationMember(orgId, uid int64) bool { - has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser)) - return has -} - -// IsPublicMembership returns true if given user public his/her membership. -func IsPublicMembership(orgId, uid int64) bool { - has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser)) - return has -} - func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) { orgs := make([]*User, 0, 10) if !showAll { @@ -287,18 +146,13 @@ func GetOrgsByUserID(userID int64, showAll bool) ([]*User, error) { return getOrgsByUserID(x.NewSession(), userID, showAll) } +// getOwnedOrgsByUserID returns a list of organizations are owned by given user ID. func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) { orgs := make([]*User, 0, 10) return orgs, sess.Where("`org_user`.uid=?", userID).And("`org_user`.is_owner=?", true). Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs) } -// GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID. -func GetOwnedOrgsByUserID(userID int64) ([]*User, error) { - sess := x.NewSession() - return getOwnedOrgsByUserID(sess, userID) -} - // GetOwnedOrganizationsByUserIDDesc returns a list of organizations are owned by // given user ID, ordered descending by the given condition. func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) { @@ -306,6 +160,7 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) { return getOwnedOrgsByUserID(sess.Desc(desc), userID) } +// getOrgUsersByOrgID returns all organization-user relations by organization ID. func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) { orgUsers := make([]*OrgUser, 0, 10) @@ -316,221 +171,9 @@ func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) { return orgUsers, sess.Find(&orgUsers) } -// GetOrgUsersByOrgID returns all organization-user relations by organization ID. -func GetOrgUsersByOrgID(orgID int64, limit int) ([]*OrgUser, error) { - return getOrgUsersByOrgID(x, orgID, limit) -} - -// ChangeOrgUserStatus changes public or private membership status. -func ChangeOrgUserStatus(orgID, uid int64, public bool) error { - ou := new(OrgUser) - has, err := x.Where("uid=?", uid).And("org_id=?", orgID).Get(ou) - if err != nil { - return err - } else if !has { - return nil - } - - ou.IsPublic = public - _, err = x.Id(ou.ID).AllCols().Update(ou) - return err -} - -// AddOrgUser adds new user to given organization. -func AddOrgUser(orgID, uid int64) error { - if IsOrganizationMember(orgID, uid) { - return nil - } - - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { - return err - } - - ou := &OrgUser{ - Uid: uid, - OrgID: orgID, - } - - if _, err := sess.Insert(ou); err != nil { - return err - } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil { - return err - } - - return sess.Commit() -} - -// RemoveOrgUser removes user from given organization. -func RemoveOrgUser(orgID, userID int64) error { - ou := new(OrgUser) - - has, err := x.Where("uid=?", userID).And("org_id=?", orgID).Get(ou) - if err != nil { - return fmt.Errorf("get org-user: %v", err) - } else if !has { - return nil - } - - user, err := Users.GetByID(context.TODO(), userID) - if err != nil { - return fmt.Errorf("GetUserByID [%d]: %v", userID, err) - } - org, err := Users.GetByID(context.TODO(), orgID) - if err != nil { - return fmt.Errorf("GetUserByID [%d]: %v", orgID, err) - } - - // FIXME: only need to get IDs here, not all fields of repository. - repos, _, err := org.GetUserRepositories(user.ID, 1, org.NumRepos) - if err != nil { - return fmt.Errorf("GetUserRepositories [%d]: %v", user.ID, err) - } - - // Check if the user to delete is the last member in owner team. - if IsOrganizationOwner(orgID, userID) { - t, err := org.GetOwnerTeam() - if err != nil { - return err - } - if t.NumMembers == 1 { - return ErrLastOrgOwner{UID: userID} - } - } - - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { - return err - } - - if _, err := sess.ID(ou.ID).Delete(ou); err != nil { - return err - } else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil { - return err - } - - // Delete all repository accesses and unwatch them. - repoIDs := make([]int64, len(repos)) - for i := range repos { - repoIDs = append(repoIDs, repos[i].ID) - if err = watchRepo(sess, user.ID, repos[i].ID, false); err != nil { - return err - } - } - - if len(repoIDs) > 0 { - if _, err = sess.Where("user_id = ?", user.ID).In("repo_id", repoIDs).Delete(new(Access)); err != nil { - return err - } - } - - // Delete member in his/her teams. - teams, err := getUserTeams(sess, org.ID, user.ID) - if err != nil { - return err - } - for _, t := range teams { - if err = removeTeamMember(sess, org.ID, t.ID, user.ID); err != nil { - return err - } - } - - return sess.Commit() -} - -func removeOrgRepo(e Engine, orgID, repoID int64) error { - _, err := e.Delete(&TeamRepo{ - OrgID: orgID, - RepoID: repoID, - }) - return err -} - -// RemoveOrgRepo removes all team-repository relations of given organization. -func RemoveOrgRepo(orgID, repoID int64) error { - return removeOrgRepo(x, orgID, repoID) -} - -func (org *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) { - teams := make([]*Team, 0, org.NumTeams) - return teams, e.Where("team_user.org_id = ?", org.ID). - And("team_user.uid = ?", userID). - Join("INNER", "team_user", "team_user.team_id = team.id"). - Cols(cols...).Find(&teams) -} - -// GetUserTeamIDs returns of all team IDs of the organization that user is member of. -func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) { - teams, err := org.getUserTeams(x, userID, "team.id") - if err != nil { - return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err) - } - - teamIDs := make([]int64, len(teams)) - for i := range teams { - teamIDs[i] = teams[i].ID - } - return teamIDs, nil -} - -// GetTeams returns all teams that belong to organization, -// and that the user has joined. -func (org *User) GetUserTeams(userID int64) ([]*Team, error) { - return org.getUserTeams(x, userID) -} - -// GetUserRepositories returns a range of repositories in organization which the user has access to, -// and total number of records based on given condition. -func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repository, int64, error) { - teamIDs, err := org.GetUserTeamIDs(userID) - if err != nil { - return nil, 0, fmt.Errorf("GetUserTeamIDs: %v", err) - } - if len(teamIDs) == 0 { - // user has no team but "IN ()" is invalid SQL - teamIDs = []int64{-1} // there is no team with id=-1 - } - - var teamRepoIDs []int64 - if err = x.Table("team_repo").In("team_id", teamIDs).Distinct("repo_id").Find(&teamRepoIDs); err != nil { - return nil, 0, fmt.Errorf("get team repository IDs: %v", err) - } - if len(teamRepoIDs) == 0 { - // team has no repo but "IN ()" is invalid SQL - teamRepoIDs = []int64{-1} // there is no repo with id=-1 - } - - if page <= 0 { - page = 1 - } - repos := make([]*Repository, 0, pageSize) - if err = x.Where("owner_id = ?", org.ID). - And(builder.Or( - builder.And(builder.Expr("is_private = ?", false), builder.Expr("is_unlisted = ?", false)), - builder.In("id", teamRepoIDs))). - Desc("updated_unix"). - Limit(pageSize, (page-1)*pageSize). - Find(&repos); err != nil { - return nil, 0, fmt.Errorf("get user repositories: %v", err) - } - - repoCount, err := x.Where("owner_id = ?", org.ID). - And(builder.Or( - builder.Expr("is_private = ?", false), - builder.In("id", teamRepoIDs))). - Count(new(Repository)) - if err != nil { - return nil, 0, fmt.Errorf("count user repositories: %v", err) - } - - return repos, repoCount, nil -} - // GetUserMirrorRepositories returns mirror repositories of the organization which the user has access to. -func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) { - teamIDs, err := org.GetUserTeamIDs(userID) +func (u *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) { + teamIDs, err := u.GetUserTeamIDs(userID) if err != nil { return nil, fmt.Errorf("GetUserTeamIDs: %v", err) } @@ -549,7 +192,7 @@ func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) } repos := make([]*Repository, 0, 10) - if err = x.Where("owner_id = ?", org.ID). + if err = x.Where("owner_id = ?", u.ID). And("is_private = ?", false). Or(builder.In("id", teamRepoIDs)). And("is_mirror = ?", true). // Don't move up because it's an independent condition diff --git a/internal/db/org_team.go b/internal/db/org_team.go index d737167e463..c79a38f3321 100644 --- a/internal/db/org_team.go +++ b/internal/db/org_team.go @@ -9,24 +9,24 @@ import ( "fmt" "strings" + "github.com/pkg/errors" "xorm.io/xorm" - "gogs.io/gogs/internal/db/errors" - "gogs.io/gogs/internal/errutil" + dberrors "gogs.io/gogs/internal/db/errors" ) -const OWNER_TEAM = "Owners" +const TeamNameOwners = "Owners" // Team represents a organization team. type Team struct { - ID int64 - OrgID int64 `xorm:"INDEX"` + ID int64 `gorm:"primaryKey"` + OrgID int64 `xorm:"INDEX" gorm:"index"` LowerName string Name string Description string Authorize AccessMode - Repos []*Repository `xorm:"-" json:"-"` - Members []*User `xorm:"-" json:"-"` + Repos []*Repository `xorm:"-" json:"-" gorm:"-"` + Members []*User `xorm:"-" json:"-" gorm:"-"` NumRepos int NumMembers int } @@ -43,7 +43,7 @@ func (t *Team) AfterSet(colName string, _ xorm.Cell) { // IsOwnerTeam returns true if team is owner team. func (t *Team) IsOwnerTeam() bool { - return t.Name == OWNER_TEAM + return t.Name == TeamNameOwners } // HasWriteAccess returns true if team has at least write level access mode. @@ -136,7 +136,7 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) { // AddRepository adds new repository to team of organization. func (t *Team) AddRepository(repo *Repository) (err error) { if repo.OwnerID != t.OrgID { - return errors.New("Repository does not belong to organization") + return dberrors.New("Repository does not belong to organization") } else if t.HasRepository(repo.ID) { return nil } @@ -259,9 +259,9 @@ func IsUsableTeamName(name string) error { // It's caller's responsibility to assign organization ID. func NewTeam(t *Team) error { if t.Name == "" { - return errors.New("empty team name") + return dberrors.New("empty team name") } else if t.OrgID == 0 { - return errors.New("OrgID is not assigned") + return dberrors.New("OrgID is not assigned") } if err := IsUsableTeamName(t.Name); err != nil { @@ -272,7 +272,7 @@ func NewTeam(t *Team) error { if err != nil { return err } else if !has { - return ErrOrgNotExist + return errors.New("organization does not exist") } t.LowerName = strings.ToLower(t.Name) @@ -301,44 +301,6 @@ func NewTeam(t *Team) error { return sess.Commit() } -var _ errutil.NotFound = (*ErrTeamNotExist)(nil) - -type ErrTeamNotExist struct { - args map[string]any -} - -func IsErrTeamNotExist(err error) bool { - _, ok := err.(ErrTeamNotExist) - return ok -} - -func (err ErrTeamNotExist) Error() string { - return fmt.Sprintf("team does not exist: %v", err.args) -} - -func (ErrTeamNotExist) NotFound() bool { - return true -} - -func getTeamOfOrgByName(e Engine, orgID int64, name string) (*Team, error) { - t := &Team{ - OrgID: orgID, - LowerName: strings.ToLower(name), - } - has, err := e.Get(t) - if err != nil { - return nil, err - } else if !has { - return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}} - } - return t, nil -} - -// GetTeamOfOrgByName returns team by given team name and organization. -func GetTeamOfOrgByName(orgID int64, name string) (*Team, error) { - return getTeamOfOrgByName(x, orgID, name) -} - func getTeamByID(e Engine, teamID int64) (*Team, error) { t := new(Team) has, err := e.ID(teamID).Get(t) @@ -355,20 +317,16 @@ func GetTeamByID(teamID int64) (*Team, error) { return getTeamByID(x, teamID) } +// getTeamsByOrgID returns all teams belong to given organization. func getTeamsByOrgID(e Engine, orgID int64) ([]*Team, error) { teams := make([]*Team, 0, 3) return teams, e.Where("org_id = ?", orgID).Find(&teams) } -// GetTeamsByOrgID returns all teams belong to given organization. -func GetTeamsByOrgID(orgID int64) ([]*Team, error) { - return getTeamsByOrgID(x, orgID) -} - // UpdateTeam updates information of team. func UpdateTeam(t *Team, authChanged bool) (err error) { if t.Name == "" { - return errors.New("empty team name") + return dberrors.New("empty team name") } if len(t.Description) > 255 { @@ -528,7 +486,7 @@ func AddTeamMember(orgID, teamID, userID int64) error { return nil } - if err := AddOrgUser(orgID, userID); err != nil { + if err := Orgs.AddMember(context.TODO(), orgID, userID); err != nil { return err } @@ -583,8 +541,8 @@ func AddTeamMember(orgID, teamID, userID int64) error { return sess.Commit() } -func removeTeamMember(e Engine, orgID, teamID, uid int64) error { - if !isTeamMember(e, orgID, teamID, uid) { +func removeTeamMember(e Engine, orgID, teamID, userID int64) error { + if !isTeamMember(e, orgID, teamID, userID) { return nil } @@ -596,7 +554,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error { // Check if the user to delete is the last member in owner team. if t.IsOwnerTeam() && t.NumMembers == 1 { - return ErrLastOrgOwner{UID: uid} + return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}} } t.NumMembers-- @@ -612,7 +570,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error { } tu := &TeamUser{ - UID: uid, + UID: userID, OrgID: orgID, TeamID: teamID, } @@ -631,7 +589,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error { // This must exist. ou := new(OrgUser) - _, err = e.Where("uid = ?", uid).And("org_id = ?", org.ID).Get(ou) + _, err = e.Where("uid = ?", userID).And("org_id = ?", org.ID).Get(ou) if err != nil { return err } @@ -673,16 +631,13 @@ type TeamRepo struct { RepoID int64 `xorm:"UNIQUE(s)"` } +// hasTeamRepo returns true if given team has access to the repository of the organization. func hasTeamRepo(e Engine, orgID, teamID, repoID int64) bool { has, _ := e.Where("org_id = ?", orgID).And("team_id = ?", teamID).And("repo_id = ?", repoID).Get(new(TeamRepo)) return has } -// HasTeamRepo returns true if given team has access to the repository of the organization. -func HasTeamRepo(orgID, teamID, repoID int64) bool { - return hasTeamRepo(x, orgID, teamID, repoID) -} - +// addTeamRepo adds new repository relation to team. func addTeamRepo(e Engine, orgID, teamID, repoID int64) error { _, err := e.InsertOne(&TeamRepo{ OrgID: orgID, @@ -692,11 +647,7 @@ func addTeamRepo(e Engine, orgID, teamID, repoID int64) error { return err } -// AddTeamRepo adds new repository relation to team. -func AddTeamRepo(orgID, teamID, repoID int64) error { - return addTeamRepo(x, orgID, teamID, repoID) -} - +// removeTeamRepo deletes repository relation to team. func removeTeamRepo(e Engine, teamID, repoID int64) error { _, err := e.Delete(&TeamRepo{ TeamID: teamID, @@ -705,11 +656,6 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error { return err } -// RemoveTeamRepo deletes repository relation to team. -func RemoveTeamRepo(teamID, repoID int64) error { - return removeTeamRepo(x, teamID, repoID) -} - // GetTeamsHaveAccessToRepo returns all teams in an organization that have given access level to the repository. func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) { teams := make([]*Team, 0, 5) @@ -719,3 +665,46 @@ func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, er And("team_repo.repo_id = ?", repoID). Find(&teams) } + +func (u *User) getTeams(e Engine) (err error) { + u.Teams, err = getTeamsByOrgID(e, u.ID) + return err +} + +// GetTeams returns all teams that belong to organization. +func (u *User) GetTeams() error { + return u.getTeams(x) +} + +// TeamsHaveAccessToRepo returns all teams that have given access level to the repository. +func (u *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) { + return GetTeamsHaveAccessToRepo(u.ID, repoID, mode) +} + +func (u *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) { + teams := make([]*Team, 0, u.NumTeams) + return teams, e.Where("team_user.org_id = ?", u.ID). + And("team_user.uid = ?", userID). + Join("INNER", "team_user", "team_user.team_id = team.id"). + Cols(cols...).Find(&teams) +} + +// GetUserTeamIDs returns of all team IDs of the organization that user is member of. +func (u *User) GetUserTeamIDs(userID int64) ([]int64, error) { + teams, err := u.getUserTeams(x, userID, "team.id") + if err != nil { + return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err) + } + + teamIDs := make([]int64, len(teams)) + for i := range teams { + teamIDs[i] = teams[i].ID + } + return teamIDs, nil +} + +// GetTeams returns all teams that belong to organization, +// and that the user has joined. +func (u *User) GetUserTeams(userID int64) ([]*Team, error) { + return u.getUserTeams(x, userID) +} diff --git a/internal/db/orgs.go b/internal/db/orgs.go index 753d81209a5..0550d07a3ae 100644 --- a/internal/db/orgs.go +++ b/internal/db/orgs.go @@ -6,26 +6,57 @@ package db import ( "context" + "fmt" + "strings" "github.com/pkg/errors" "gorm.io/gorm" "gogs.io/gogs/internal/dbutil" + "gogs.io/gogs/internal/errutil" ) // OrgsStore is the persistent interface for organizations. type OrgsStore interface { - // List returns a list of organizations filtered by options. - List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) + // AddMember adds a new member to the given organization. + AddMember(ctx context.Context, orgID, userID int64) error + // RemoveMember removes a member from the given organization. + RemoveMember(ctx context.Context, orgID, userID int64) error + // HasMember returns whether the given user is a member of the organization + // (first), and whether the organization membership is public (second). + HasMember(ctx context.Context, orgID, userID int64) (bool, bool) + // ListMembers returns all members of the given organization, and sorted by the + // given order (e.g. "id ASC"). + ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) + // IsOwnedBy returns true if the given user is an owner of the organization. + IsOwnedBy(ctx context.Context, orgID, userID int64) bool + // SetMemberVisibility sets the visibility of the given user in the organization. + SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error + + // GetByName returns the organization with given name. + GetByName(ctx context.Context, name string) (*Organization, error) // SearchByName returns a list of organizations whose username or full name // matches the given keyword case-insensitively. Results are paginated by given // page and page size, and sorted by the given order (e.g. "id DESC"). A total // count of all results is also returned. If the order is not given, it's up to // the database to decide. SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) - + // List returns a list of organizations filtered by options. + List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) // CountByUser returns the number of organizations the user is a member of. CountByUser(ctx context.Context, userID int64) (int64, error) + // Count returns the total number of organizations. + Count(ctx context.Context) int64 + + // GetTeamByName returns the team with given name under the given organization. + // It returns ErrTeamNotExist whe not found. + GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) + + // AccessibleRepositoriesByUser returns a range of repositories in the + // organization that the user has access to and the total number of it. Results + // are paginated by given page and page size, and sorted by the given order + // (e.g. "updated_unix DESC"). + AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) } var Orgs OrgsStore @@ -42,6 +73,260 @@ func NewOrgsStore(db *gorm.DB) OrgsStore { return &orgs{DB: db} } +func (*orgs) recountMembers(tx *gorm.DB, orgID int64) error { + /* + Equivalent SQL for PostgreSQL: + + UPDATE "user" + SET num_members = ( + SELECT COUNT(*) FROM org_user WHERE org_id = @orgID + ) + WHERE id = @orgID + */ + err := tx.Model(&User{}). + Where("id = ?", orgID). + Update( + "num_members", + tx.Model(&OrgUser{}).Select("COUNT(*)").Where("org_id = ?", orgID), + ). + Error + if err != nil { + return errors.Wrap(err, `update "user.num_members"`) + } + return nil +} + +func (db *orgs) AddMember(ctx context.Context, orgID, userID int64) error { + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + ou := &OrgUser{ + UserID: userID, + OrgID: orgID, + } + result := tx.FirstOrCreate(ou, ou) + if result.Error != nil { + return errors.Wrap(result.Error, "upsert") + } else if result.RowsAffected <= 0 { + return nil // Relation already exists + } + return db.recountMembers(tx, orgID) + }) +} + +type ErrLastOrgOwner struct { + args map[string]any +} + +func IsErrLastOrgOwner(err error) bool { + return errors.As(err, &ErrLastOrgOwner{}) +} + +func (err ErrLastOrgOwner) Error() string { + return fmt.Sprintf("user is the last owner of the organization: %v", err.args) +} + +func (db *orgs) RemoveMember(ctx context.Context, orgID, userID int64) error { + ou, err := db.getOrgUser(ctx, orgID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // Not a member + } + return errors.Wrap(err, "check organization membership") + } + + // Check if the member to remove is the last owner. + if ou.IsOwner { + t, err := db.GetTeamByName(ctx, orgID, TeamNameOwners) + if err != nil { + return errors.Wrap(err, "get owners team") + } else if t.NumMembers == 1 { + return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}} + } + } + + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + repoIDsConds := db.accessibleRepositoriesByUser(tx, orgID, userID, accessibleRepositoriesByUserOptions{}).Select("repository.id") + + err := tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Watch{}).Error + if err != nil { + return errors.Wrap(err, "unwatch repositories") + } + + err = tx.Table("repository"). + Where("id IN (?)", repoIDsConds). + UpdateColumn("num_watches", gorm.Expr("num_watches - 1")). + Error + if err != nil { + return errors.Wrap(err, `decrease "repository.num_watches"`) + } + + err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Access{}).Error + if err != nil { + return errors.Wrap(err, "delete repository accesses") + } + + err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Collaboration{}).Error + if err != nil { + return errors.Wrap(err, "delete repository collaborations") + } + + /* + Equivalent SQL for PostgreSQL: + + UPDATE "team" + SET num_members = num_members - 1 + WHERE id IN ( + SELECT team_id FROM "team_user" + WHERE team_user.org_id = @orgID AND uid = @userID) + ) + */ + err = tx.Table("team"). + Where(`id IN (?)`, tx. + Select("team_id"). + Table("team_user"). + Where("org_id = ? AND uid = ?", orgID, userID), + ). + UpdateColumn("num_members", gorm.Expr("num_members - 1")). + Error + if err != nil { + return errors.Wrap(err, `decrease "team.num_members"`) + } + + err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&TeamUser{}).Error + if err != nil { + return errors.Wrap(err, "delete team membership") + } + + err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&OrgUser{}).Error + if err != nil { + return errors.Wrap(err, "delete organization membership") + } + return db.recountMembers(tx, orgID) + }) +} + +type accessibleRepositoriesByUserOptions struct { + orderBy string + page int + pageSize int +} + +func (*orgs) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID int64, opts accessibleRepositoriesByUserOptions) *gorm.DB { + /* + Equivalent SQL for PostgreSQL: + + + SELECT * FROM "repository" JOIN team_repo ON repository.id = team_repo.repo_id WHERE owner_id = @orgID @@ -250,14 +252,14 @@ func (*organizations) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID in [LIMIT @limit OFFSET @offset] */ conds := tx. + Table("repository"). Joins("JOIN team_repo ON repository.id = team_repo.repo_id"). - Where("owner_id = ? AND (?)", orgID, tx. - Where("team_repo.team_id IN (?)", tx. - Select("team_id"). + Where("owner_id = ? AND (team_repo.team_id IN (?) OR (repository.is_private = ? AND repository.is_unlisted = ?))", + orgID, + tx.Select("team_id"). Table("team_user"). Where("team_user.org_id = ? AND uid = ?", orgID, userID), - ). - Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false), + false, false, ) if opts.orderBy == OrderByUpdatedDesc { conds.Order("updated_unix DESC") diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index e8cde59c4fe..4aa178fcc75 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -29,7 +29,7 @@ func TestOrganizations(t *testing.T) { tables := []any{ new(User), new(EmailAddress), new(OrgUser), new(Team), new(TeamUser), new(Repository), new(Watch), new(Star), new(Follow), new(Issue), new(PublicKey), new(AccessToken), new(Collaboration), new(Access), new(Action), - new(IssueUser), + new(IssueUser), new(TeamRepo), } db := &organizations{ DB: dbtest.NewDB(t, "orgs", tables...), @@ -47,6 +47,7 @@ func TestOrganizations(t *testing.T) { {"Count", orgsCount}, {"DeleteByID", orgsDeleteByID}, {"AddMember", orgsAddMember}, + {"RemoveMember", orgsRemoveMember}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -415,21 +416,129 @@ func orgsAddMember(t *testing.T, db *organizations) { require.NoError(t, err) // Not yet a member - got, err := db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) + gotOrgs, err := db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) require.NoError(t, err) - assert.Len(t, got, 0) + assert.Len(t, gotOrgs, 0) // Add member err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) // Now a member - got, err = db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) + gotOrgs, err = db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) require.NoError(t, err) - assert.Len(t, got, 1) - assert.Equal(t, org1.ID, got[0].ID) + assert.Len(t, gotOrgs, 1) + assert.Equal(t, org1.ID, gotOrgs[0].ID) // Add member again shouldn't fail err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) + + gotOrg, err := db.GetByName(ctx, org1.Name) + require.NoError(t, err) + assert.Equal(t, 2, gotOrg.NumMembers) +} + +func orgsRemoveMember(t *testing.T, db *organizations) { + ctx := context.Background() + + usersStore := NewUsersStore(db.DB) + alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) + require.NoError(t, err) + bob, err := usersStore.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsRemoveMember-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + + org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + + t.Run("remove non-existent member", func(t *testing.T) { + err = db.RemoveMember(ctx, org1.ID, bob.ID) + require.NoError(t, err) + }) + + t.Run("remove last owner", func(t *testing.T) { + err = db.RemoveMember(ctx, org1.ID, alice.ID) + wantErr := ErrLastOrgOwner{errutil.Args{"orgID": org1.ID, "userID": alice.ID}} + assert.Equal(t, wantErr, err) + }) + + err = db.AddMember(ctx, org1.ID, bob.ID) + require.NoError(t, err) + + // Mock repository, watches, accesses and collaborations + reposStore := NewRepositoriesStore(db.DB) + repo1, err := reposStore.Create(ctx, org1.ID, CreateRepoOptions{Name: "repo1", Private: true}) + require.NoError(t, err) + err = reposStore.Watch(ctx, bob.ID, repo1.ID) + require.NoError(t, err) + permsStore := NewPermsStore(db.DB) + err = permsStore.SetRepoPerms(ctx, repo1.ID, map[int64]AccessMode{bob.ID: AccessModeRead}) + require.NoError(t, err) + // TODO: Use Repositories.AddCollaborator to replace SQL hack when the method is available. + err = db.DB.Create( + &Collaboration{ + UserID: bob.ID, + RepoID: repo1.ID, + Mode: AccessModeRead, + }, + ).Error + require.NoError(t, err) + + // Mock team membership + // TODO: Use Organizations.CreateTeam to replace SQL hack when the method is available. + team1 := &Team{ + OrgID: org1.ID, + LowerName: "team1", + Name: "team1", + NumMembers: 1, + } + err = db.DB.Create(team1).Error + require.NoError(t, err) + // TODO: Use Organizations.AddTeamMember to replace SQL hack when the method is available. + err = db.DB.Create( + &TeamUser{ + OrgID: org1.ID, + TeamID: team1.ID, + UID: bob.ID, + }, + ).Error + require.NoError(t, err) + // TODO: Use Organizations.AddTeamRepository to replace SQL hack when the method is available. + err = db.DB.Create( + &TeamRepo{ + OrgID: org1.ID, + TeamID: team1.ID, + RepoID: repo1.ID, + }, + ).Error + require.NoError(t, err) + + // Pull the trigger + err = db.RemoveMember(ctx, org1.ID, bob.ID) + require.NoError(t, err) + + // Verify after-the-fact data + gotRepo, err := reposStore.GetByID(ctx, repo1.ID) + require.NoError(t, err) + assert.Equal(t, 1, gotRepo.NumWatches) + + gotAccessMode := permsStore.AccessMode(ctx, repo1.ID, bob.ID, AccessModeOptions{Private: repo1.IsPrivate}) + assert.Equal(t, AccessModeNone, gotAccessMode) + + // TODO: Use Repositories.ListCollaborators to replace SQL hack when the method is available. + var count int64 + err = db.DB.Model(&Collaboration{}).Where(&Collaboration{RepoID: repo1.ID}).Count(&count).Error + require.NoError(t, err) + assert.Equal(t, int64(0), count) + + gotTeam, err := db.GetTeamByName(ctx, org1.ID, team1.Name) + require.NoError(t, err) + assert.Equal(t, 0, gotTeam.NumMembers) + + gotOrg, err := db.GetByName(ctx, org1.Name) + require.NoError(t, err) + assert.Equal(t, 1, gotOrg.NumMembers) } diff --git a/internal/db/users.go b/internal/db/users.go index 53be95965da..81623928d8b 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -526,7 +526,8 @@ func (db *users) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthor SELECT repo_id FROM watch WHERE user_id = @userID ) */ - err = tx.Table("repository"). + err = tx. + Table("repository"). Where("id IN (?)", tx. Select("repo_id"). Table("watch"). @@ -547,7 +548,8 @@ func (db *users) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthor SELECT repo_id FROM star WHERE uid = @userID ) */ - err = tx.Table("repository"). + err = tx. + Table("repository"). Where("id IN (?)", tx. Select("repo_id"). Table("star"). @@ -568,7 +570,8 @@ func (db *users) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthor SELECT follow_id FROM follow WHERE user_id = @userID ) */ - err = tx.Table("user"). + err = tx. + Table("user"). Where("id IN (?)", tx. Select("follow_id"). Table("follow"). @@ -589,7 +592,8 @@ func (db *users) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthor SELECT user_id FROM follow WHERE follow_id = @userID ) */ - err = tx.Table("user"). + err = tx. + Table("user"). Where("id IN (?)", tx. Select("user_id"). Table("follow"). From 7d4c5b47a266799d8fe5f3f10ca663f6148b8183 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 15 Nov 2023 22:22:52 -0500 Subject: [PATCH 22/29] Check access before watch repo --- internal/db/organizations_test.go | 19 ++++++--- internal/db/repositories.go | 43 ++++++++++++++++--- internal/db/repositories_test.go | 68 ++++++++++++++++++++++++++----- internal/db/users_test.go | 10 ++++- 4 files changed, 117 insertions(+), 23 deletions(-) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 4aa178fcc75..1aefe4969ed 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -472,11 +472,6 @@ func orgsRemoveMember(t *testing.T, db *organizations) { reposStore := NewRepositoriesStore(db.DB) repo1, err := reposStore.Create(ctx, org1.ID, CreateRepoOptions{Name: "repo1", Private: true}) require.NoError(t, err) - err = reposStore.Watch(ctx, bob.ID, repo1.ID) - require.NoError(t, err) - permsStore := NewPermsStore(db.DB) - err = permsStore.SetRepoPerms(ctx, repo1.ID, map[int64]AccessMode{bob.ID: AccessModeRead}) - require.NoError(t, err) // TODO: Use Repositories.AddCollaborator to replace SQL hack when the method is available. err = db.DB.Create( &Collaboration{ @@ -516,6 +511,20 @@ func orgsRemoveMember(t *testing.T, db *organizations) { ).Error require.NoError(t, err) + permsStore := NewPermsStore(db.DB) + err = permsStore.SetRepoPerms(ctx, repo1.ID, map[int64]AccessMode{bob.ID: AccessModeRead}) + require.NoError(t, err) + err = reposStore.Watch( + ctx, + WatchRepositoryOptions{ + UserID: bob.ID, + RepoID: repo1.ID, + RepoOwnerID: repo1.OwnerID, + RepoIsPrivate: repo1.IsPrivate, + }, + ) + require.NoError(t, err) + // Pull the trigger err = db.RemoveMember(ctx, org1.ID, bob.ID) require.NoError(t, err) diff --git a/internal/db/repositories.go b/internal/db/repositories.go index c11a828b397..3c03034b6fe 100644 --- a/internal/db/repositories.go +++ b/internal/db/repositories.go @@ -50,7 +50,7 @@ type RepositoriesStore interface { // ListWatches returns all watches of the given repository. ListWatches(ctx context.Context, repoID int64) ([]*Watch, error) // Watch marks the user to watch the repository. - Watch(ctx context.Context, userID, repoID int64) error + Watch(ctx context.Context, opts WatchRepositoryOptions) error // HasForkedBy returns true if the given repository has forked by the given user. HasForkedBy(ctx context.Context, repoID, userID int64) bool @@ -194,7 +194,15 @@ func (db *repositories) Create(ctx context.Context, ownerID int64, opts CreateRe return errors.Wrap(err, "create") } - err = NewRepositoriesStore(tx).Watch(ctx, ownerID, repo.ID) + err = NewRepositoriesStore(tx).Watch( + ctx, + WatchRepositoryOptions{ + UserID: ownerID, + RepoID: repo.ID, + RepoOwnerID: ownerID, + RepoIsPrivate: repo.IsPrivate, + }, + ) if err != nil { return errors.Wrap(err, "watch") } @@ -400,11 +408,34 @@ func (db *repositories) recountWatches(tx *gorm.DB, repoID int64) error { Error } -func (db *repositories) Watch(ctx context.Context, userID, repoID int64) error { +type WatchRepositoryOptions struct { + UserID int64 + RepoID int64 + RepoOwnerID int64 + RepoIsPrivate bool +} + +func (db *repositories) Watch(ctx context.Context, opts WatchRepositoryOptions) error { + // Make sure the user has access to the private repository + if opts.RepoIsPrivate && + opts.UserID != opts.RepoOwnerID && + !NewPermsStore(db.DB).Authorize( + ctx, + opts.UserID, + opts.RepoID, + AccessModeRead, + AccessModeOptions{ + OwnerID: opts.RepoOwnerID, + Private: true, + }, + ) { + return errors.New("user does not have access to the repository") + } + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { w := &Watch{ - UserID: userID, - RepoID: repoID, + UserID: opts.UserID, + RepoID: opts.RepoID, } result := tx.FirstOrCreate(w, w) if result.Error != nil { @@ -413,7 +444,7 @@ func (db *repositories) Watch(ctx context.Context, userID, repoID int64) error { return nil // Relation already exists } - return db.recountWatches(tx, repoID) + return db.recountWatches(tx, opts.RepoID) }) } diff --git a/internal/db/repositories_test.go b/internal/db/repositories_test.go index ab0427ca0f5..1ab2b1dc75a 100644 --- a/internal/db/repositories_test.go +++ b/internal/db/repositories_test.go @@ -290,11 +290,20 @@ func reposTouch(t *testing.T, ctx context.Context, db *repositories) { } func reposListWatches(t *testing.T, ctx context.Context, db *repositories) { - err := db.Watch(ctx, 1, 1) + repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1"}) require.NoError(t, err) - err = db.Watch(ctx, 2, 1) + _, err = db.Create(ctx, 2, CreateRepoOptions{Name: "repo2"}) require.NoError(t, err) - err = db.Watch(ctx, 2, 2) + + err = db.Watch( + ctx, + WatchRepositoryOptions{ + UserID: 2, + RepoID: repo1.ID, + RepoOwnerID: repo1.OwnerID, + RepoIsPrivate: repo1.IsPrivate, + }, + ) require.NoError(t, err) got, err := db.ListWatches(ctx, 1) @@ -314,16 +323,53 @@ func reposWatch(t *testing.T, ctx context.Context, db *repositories) { repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1"}) require.NoError(t, err) - err = db.Watch(ctx, 2, repo1.ID) - require.NoError(t, err) + t.Run("user does not have access to the repository", func(t *testing.T) { + repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1", Private: true}) + require.NoError(t, err) - // It is OK to watch multiple times and just be noop. - err = db.Watch(ctx, 2, repo1.ID) - require.NoError(t, err) + err = db.Watch( + ctx, + WatchRepositoryOptions{ + UserID: 2, + RepoID: repo1.ID, + RepoOwnerID: repo1.OwnerID, + RepoIsPrivate: repo1.IsPrivate, + }, + ) + require.Error(t, err) + }) - repo1, err = db.GetByID(ctx, repo1.ID) - require.NoError(t, err) - assert.Equal(t, 2, repo1.NumWatches) // The owner is watching the repo by default. + t.Run("user has access to the repository", func(t *testing.T) { + repo2, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo2"}) + require.NoError(t, err) + + err = db.Watch( + ctx, + WatchRepositoryOptions{ + UserID: 2, + RepoID: repo2.ID, + RepoOwnerID: repo2.OwnerID, + RepoIsPrivate: repo2.IsPrivate, + }, + ) + require.NoError(t, err) + + // It is OK to watch multiple times and just be noop. + err = db.Watch( + ctx, + WatchRepositoryOptions{ + UserID: 2, + RepoID: repo2.ID, + RepoOwnerID: repo2.OwnerID, + RepoIsPrivate: repo2.IsPrivate, + }, + ) + require.NoError(t, err) + + repo2, err = db.GetByID(ctx, repo2.ID) + require.NoError(t, err) + assert.Equal(t, 2, repo2.NumWatches) // The owner is watching the repo by default. + }) } func reposHasForkedBy(t *testing.T, ctx context.Context, db *repositories) { diff --git a/internal/db/users_test.go b/internal/db/users_test.go index 1edc09f822a..4a5d65907a7 100644 --- a/internal/db/users_test.go +++ b/internal/db/users_test.go @@ -513,7 +513,15 @@ func usersDeleteByID(t *testing.T, ctx context.Context, db *users) { require.NoError(t, err) // Mock watches, stars and follows - err = reposStore.Watch(ctx, testUser.ID, repo2.ID) + err = reposStore.Watch( + ctx, + WatchRepositoryOptions{ + UserID: testUser.ID, + RepoID: repo2.ID, + RepoOwnerID: repo2.OwnerID, + RepoIsPrivate: repo2.IsPrivate, + }, + ) require.NoError(t, err) err = reposStore.Star(ctx, testUser.ID, repo2.ID) require.NoError(t, err) From b2a0ce6b530f7ef5adc15f898b7b3c7796564837 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Wed, 15 Nov 2023 22:26:20 -0500 Subject: [PATCH 23/29] orgsHasMember --- internal/db/organizations_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 1aefe4969ed..8e9876e8a41 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -48,6 +48,7 @@ func TestOrganizations(t *testing.T) { {"DeleteByID", orgsDeleteByID}, {"AddMember", orgsAddMember}, {"RemoveMember", orgsRemoveMember}, + {"HasMember", orgsHasMember}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -551,3 +552,16 @@ func orgsRemoveMember(t *testing.T, db *organizations) { require.NoError(t, err) assert.Equal(t, 1, gotOrg.NumMembers) } + +func orgsHasMember(t *testing.T, db *organizations) { + ctx := context.Background() + + got, _ := db.HasMember(ctx, 1, 1) + assert.False(t, got) + + err := db.AddMember(ctx, 1, 1) + require.NoError(t, err) + + got, _ = db.HasMember(ctx, 1, 1) + assert.True(t, got) +} From 54d5e4c3996d2c0baea0adb3a778d0078a1a9193 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 03:27:01 +0000 Subject: [PATCH 24/29] style: format code with Go fmt and Gofumpt This commit fixes the style issues introduced in 12efef4 according to the output from Go fmt and Gofumpt. Details: https://github.com/gogs/gogs/pull/7538 --- internal/db/organizations_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 8e9876e8a41..9040874fa76 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -561,7 +561,7 @@ func orgsHasMember(t *testing.T, db *organizations) { err := db.AddMember(ctx, 1, 1) require.NoError(t, err) - + got, _ = db.HasMember(ctx, 1, 1) assert.True(t, got) } From c5fa4cf93deb0bdfc6f4dbda0a93573419003479 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 16 Nov 2023 22:49:19 -0500 Subject: [PATCH 25/29] orgsListMembers --- internal/db/organizations_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 9040874fa76..a7b4a07fd6a 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -49,6 +49,7 @@ func TestOrganizations(t *testing.T) { {"AddMember", orgsAddMember}, {"RemoveMember", orgsRemoveMember}, {"HasMember", orgsHasMember}, + {"ListMembers", orgsListMembers}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -565,3 +566,32 @@ func orgsHasMember(t *testing.T, db *organizations) { got, _ = db.HasMember(ctx, 1, 1) assert.True(t, got) } + +func orgsListMembers(t *testing.T, db *organizations) { + ctx := context.Background() + + usersStore := NewUsersStore(db.DB) + alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) + require.NoError(t, err) + bob, err := usersStore.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsListMembers-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + + org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + err = db.AddMember(ctx, org1.ID, bob.ID) + require.NoError(t, err) + + got, err := db.ListMembers(ctx, org1.ID, ListOrgMembersOptions{Limit: 1}) + require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, alice.ID, got[0].ID) + + got, err = db.ListMembers(ctx, org1.ID, ListOrgMembersOptions{}) + require.NoError(t, err) + require.Len(t, got, 2) + assert.Equal(t, alice.ID, got[0].ID) + assert.Equal(t, bob.ID, got[1].ID) +} From 22526be6cfc96c427e933997ea887986f5f89008 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 16 Nov 2023 22:51:48 -0500 Subject: [PATCH 26/29] orgsIsOwnedBy --- internal/db/organizations_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index a7b4a07fd6a..01c01262380 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -50,6 +50,7 @@ func TestOrganizations(t *testing.T) { {"RemoveMember", orgsRemoveMember}, {"HasMember", orgsHasMember}, {"ListMembers", orgsListMembers}, + {"IsOwnedBy", orgsIsOwnedBy}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -595,3 +596,30 @@ func orgsListMembers(t *testing.T, db *organizations) { assert.Equal(t, alice.ID, got[0].ID) assert.Equal(t, bob.ID, got[1].ID) } + +func orgsIsOwnedBy(t *testing.T, db *organizations) { + ctx := context.Background() + + usersStore := NewUsersStore(db.DB) + alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) + require.NoError(t, err) + bob, err := usersStore.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + cindy, err := usersStore.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsListMembers-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + + org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + err = db.AddMember(ctx, org1.ID, bob.ID) + require.NoError(t, err) + + got := db.IsOwnedBy(ctx, org1.ID, alice.ID) + assert.True(t, got) + got = db.IsOwnedBy(ctx, org1.ID, bob.ID) + assert.False(t, got) + got = db.IsOwnedBy(ctx, org1.ID, cindy.ID) + assert.False(t, got) +} From 34e59c21f071662e2357a6ad26c8635f71fbf1c1 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Thu, 16 Nov 2023 22:55:33 -0500 Subject: [PATCH 27/29] orgsSetMemberVisibility --- internal/db/organizations_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 01c01262380..6b6e9e3565f 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -51,6 +51,7 @@ func TestOrganizations(t *testing.T) { {"HasMember", orgsHasMember}, {"ListMembers", orgsListMembers}, {"IsOwnedBy", orgsIsOwnedBy}, + {"SetMemberVisibility", orgsSetMemberVisibility}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -623,3 +624,27 @@ func orgsIsOwnedBy(t *testing.T, db *organizations) { got = db.IsOwnedBy(ctx, org1.ID, cindy.ID) assert.False(t, got) } + +func orgsSetMemberVisibility(t *testing.T, db *organizations) { + ctx := context.Background() + + usersStore := NewUsersStore(db.DB) + alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) + require.NoError(t, err) + + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsListMembers-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + + org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + + got, err := db.List(ctx, ListOrganizationsOptions{MemberID: alice.ID}) + require.NoError(t, err) + assert.Len(t, got, 0) + + err = db.SetMemberVisibility(ctx, org1.ID, alice.ID, true) + require.NoError(t, err) + got, err = db.List(ctx, ListOrganizationsOptions{MemberID: alice.ID}) + require.NoError(t, err) + assert.Len(t, got, 1) +} From f0fed90a0acbc329f2b2b0f311c3f199b5ac52d2 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sat, 25 Nov 2023 23:26:56 -0500 Subject: [PATCH 28/29] Add tests --- internal/db/organizations.go | 26 +++++++------- internal/db/organizations_test.go | 60 +++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/internal/db/organizations.go b/internal/db/organizations.go index a043fc418ec..c93d0038736 100644 --- a/internal/db/organizations.go +++ b/internal/db/organizations.go @@ -238,28 +238,30 @@ func (*organizations) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID in Equivalent SQL for PostgreSQL: SELECT * FROM "repository" - JOIN team_repo ON repository.id = team_repo.repo_id WHERE owner_id = @orgID AND ( - team_repo.team_id IN ( - SELECT team_id FROM "team_user" - WHERE team_user.org_id = @orgID AND uid = @userID) + id IN ( + SELECT repo_id + FROM "team_repo" + JOIN "team_user" ON team_user.org_id = team_repo.org_id + WHERE team_repo.org_id = @orgID AND team_user.uid = @userID ) - OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE) + OR (is_private = FALSE AND is_unlisted = FALSE) ) [ORDER BY updated_unix DESC] [LIMIT @limit OFFSET @offset] */ conds := tx. Table("repository"). - Joins("JOIN team_repo ON repository.id = team_repo.repo_id"). - Where("owner_id = ? AND (team_repo.team_id IN (?) OR (repository.is_private = ? AND repository.is_unlisted = ?))", + Where("owner_id = ? AND (id IN (?) OR (is_private = ? AND is_unlisted = ?))", orgID, - tx.Select("team_id"). - Table("team_user"). - Where("team_user.org_id = ? AND uid = ?", orgID, userID), - false, false, + tx.Select("repo_id"). + Table("team_repo"). + Joins("JOIN team_user ON team_user.org_id = team_repo.org_id"). + Where("team_repo.org_id = ? AND team_user.uid = ?", orgID, userID), + false, + false, ) if opts.orderBy == OrderByUpdatedDesc { conds.Order("updated_unix DESC") @@ -297,7 +299,7 @@ func (db *organizations) AccessibleRepositoriesByUser(ctx context.Context, orgID return repos, 0, nil } var count int64 - err = conds.Model(&Repository{}).Count(&count).Error + err = conds.Count(&count).Error if err != nil { return nil, 0, errors.Wrap(err, "count repositories") } diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index 6b6e9e3565f..bfffb7b7207 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -52,6 +52,9 @@ func TestOrganizations(t *testing.T) { {"ListMembers", orgsListMembers}, {"IsOwnedBy", orgsIsOwnedBy}, {"SetMemberVisibility", orgsSetMemberVisibility}, + {"GetTeamByName", orgsGetTeamByName}, + {"AccessibleRepositoriesByUser", orgsAccessibleRepositoriesByUser}, + {"MirrorRepositoriesByUser", orgsMirrorRepositoriesByUser}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { @@ -472,7 +475,7 @@ func orgsRemoveMember(t *testing.T, db *organizations) { err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) - // Mock repository, watches, accesses and collaborations + // Mock repository, watches and collaborations reposStore := NewRepositoriesStore(db.DB) repo1, err := reposStore.Create(ctx, org1.ID, CreateRepoOptions{Name: "repo1", Private: true}) require.NoError(t, err) @@ -515,6 +518,7 @@ func orgsRemoveMember(t *testing.T, db *organizations) { ).Error require.NoError(t, err) + // Mock accesses permsStore := NewPermsStore(db.DB) err = permsStore.SetRepoPerms(ctx, repo1.ID, map[int64]AccessMode{bob.ID: AccessModeRead}) require.NoError(t, err) @@ -609,7 +613,7 @@ func orgsIsOwnedBy(t *testing.T, db *organizations) { cindy, err := usersStore.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{}) require.NoError(t, err) - tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsListMembers-tempPictureAvatarUploadPath") + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsIsOwnedBy-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) @@ -632,7 +636,7 @@ func orgsSetMemberVisibility(t *testing.T, db *organizations) { alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) - tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsListMembers-tempPictureAvatarUploadPath") + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsSetMemberVisibility-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) @@ -648,3 +652,53 @@ func orgsSetMemberVisibility(t *testing.T, db *organizations) { require.NoError(t, err) assert.Len(t, got, 1) } + +func orgsGetTeamByName(t *testing.T, db *organizations) { + ctx := context.Background() + + usersStore := NewUsersStore(db.DB) + alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) + require.NoError(t, err) + + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsGetTeamByName-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + + org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + + t.Run("non-existent team", func(t *testing.T) { + _, err := db.GetTeamByName(ctx, org1.ID, "non-existent") + wantErr := ErrTeamNotExist{errutil.Args{"orgID": org1.ID, "name": "non-existent"}} + assert.Equal(t, wantErr, err) + }) + + t.Run("team of another organization", func(t *testing.T) { + org2, err := db.Create(ctx, "org2", alice.ID, CreateOrganizationOptions{}) + require.NoError(t, err) + + // TODO: Use Organizations.CreateTeam to replace SQL hack when the method is available. + team1 := &Team{ + OrgID: org2.ID, + LowerName: "team1", + Name: "team1", + NumMembers: 1, + } + err = db.DB.Create(team1).Error + require.NoError(t, err) + + _, err = db.GetTeamByName(ctx, org1.ID, team1.Name) + wantErr := ErrTeamNotExist{errutil.Args{"orgID": org1.ID, "name": team1.Name}} + assert.Equal(t, wantErr, err) + }) + + _, err = db.GetTeamByName(ctx, org1.ID, TeamNameOwners) + require.NoError(t, err) +} + +func orgsAccessibleRepositoriesByUser(t *testing.T, db *organizations) { + // todo +} + +func orgsMirrorRepositoriesByUser(t *testing.T, db *organizations) { + // todo +} From 403e1fb21925779eb71313006556e99618947ef8 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sun, 17 Dec 2023 16:46:07 -0500 Subject: [PATCH 29/29] Fix test --- internal/db/organizations_test.go | 40 +++++++++---------------------- internal/db/repositories_test.go | 3 --- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/internal/db/organizations_test.go b/internal/db/organizations_test.go index bfffb7b7207..27b6d2aa343 100644 --- a/internal/db/organizations_test.go +++ b/internal/db/organizations_test.go @@ -324,9 +324,7 @@ func orgsCountByUser(t *testing.T, ctx context.Context, db *organizations) { assert.Equal(t, int64(0), got) } -func orgsCount(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsCount(t *testing.T, ctx context.Context, db *organizations) { // Has no organization initially got := db.Count(ctx) assert.Equal(t, int64(0), got) @@ -344,9 +342,7 @@ func orgsCount(t *testing.T, db *organizations) { assert.Equal(t, int64(1), got) } -func orgsDeleteByID(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsDeleteByID(t *testing.T, ctx context.Context, db *organizations) { tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsDeleteByID-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) @@ -407,9 +403,7 @@ func orgsDeleteByID(t *testing.T, db *organizations) { assert.Equal(t, wantErr, err) } -func orgsAddMember(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsAddMember(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -446,9 +440,7 @@ func orgsAddMember(t *testing.T, db *organizations) { assert.Equal(t, 2, gotOrg.NumMembers) } -func orgsRemoveMember(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsRemoveMember(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -560,9 +552,7 @@ func orgsRemoveMember(t *testing.T, db *organizations) { assert.Equal(t, 1, gotOrg.NumMembers) } -func orgsHasMember(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsHasMember(t *testing.T, ctx context.Context, db *organizations) { got, _ := db.HasMember(ctx, 1, 1) assert.False(t, got) @@ -573,9 +563,7 @@ func orgsHasMember(t *testing.T, db *organizations) { assert.True(t, got) } -func orgsListMembers(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsListMembers(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -602,9 +590,7 @@ func orgsListMembers(t *testing.T, db *organizations) { assert.Equal(t, bob.ID, got[1].ID) } -func orgsIsOwnedBy(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsIsOwnedBy(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -629,9 +615,7 @@ func orgsIsOwnedBy(t *testing.T, db *organizations) { assert.False(t, got) } -func orgsSetMemberVisibility(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsSetMemberVisibility(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -653,9 +637,7 @@ func orgsSetMemberVisibility(t *testing.T, db *organizations) { assert.Len(t, got, 1) } -func orgsGetTeamByName(t *testing.T, db *organizations) { - ctx := context.Background() - +func orgsGetTeamByName(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) @@ -695,10 +677,10 @@ func orgsGetTeamByName(t *testing.T, db *organizations) { require.NoError(t, err) } -func orgsAccessibleRepositoriesByUser(t *testing.T, db *organizations) { +func orgsAccessibleRepositoriesByUser(t *testing.T, ctx context.Context, db *organizations) { // todo } -func orgsMirrorRepositoriesByUser(t *testing.T, db *organizations) { +func orgsMirrorRepositoriesByUser(t *testing.T, ctx context.Context, db *organizations) { // todo } diff --git a/internal/db/repositories_test.go b/internal/db/repositories_test.go index 1ab2b1dc75a..687b916ee95 100644 --- a/internal/db/repositories_test.go +++ b/internal/db/repositories_test.go @@ -320,9 +320,6 @@ func reposListWatches(t *testing.T, ctx context.Context, db *repositories) { } func reposWatch(t *testing.T, ctx context.Context, db *repositories) { - repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1"}) - require.NoError(t, err) - t.Run("user does not have access to the repository", func(t *testing.T) { repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1", Private: true}) require.NoError(t, err) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy