// Copyright 2022 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. 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 { // 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 var _ OrgsStore = (*orgs)(nil) type orgs struct { *gorm.DB } // NewOrgsStore returns a persistent interface for orgs with given database // connection. 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: