diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9feee4e2..dae14e837 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 b49cca3e6..613f1831e 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 389677af9..51256358a 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 825cfa87a..f9b63e154 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 48d080b3d..a67cb0b8d 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 9d2d9403a..391af791a 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 9878032d3..e7dc31119 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 8436aa99e..1c831cf86 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 bff18efd1..6f9116614 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 9469a3f08..c5a7b974d 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 04c1e3636..d6cfe6f94 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 1e3b8d9b7..ebbe5136d 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 8bc16701e..4b780cdc6 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 d737167e4..c79a38f33 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 753d81209..0550d07a3 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: + +