mirror of https://github.com/gogs/gogs.git
463 lines
14 KiB
Go
463 lines
14 KiB
Go
// 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:
|
|
|
|
<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)
|
|
)
|
|
OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
|
|
)
|
|
[ORDER BY updated_unix DESC]
|
|
[LIMIT @limit OFFSET @offset]
|
|
*/
|
|
conds := tx.
|
|
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").
|
|
Table("team_user").
|
|
Where("team_user.org_id = ? AND uid = ?", orgID, userID),
|
|
).
|
|
Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
|
|
)
|
|
if opts.orderBy != "" {
|
|
conds.Order(opts.orderBy)
|
|
}
|
|
if opts.page > 0 && opts.pageSize > 0 {
|
|
conds.Limit(opts.pageSize).Offset((opts.page - 1) * opts.pageSize)
|
|
}
|
|
return conds
|
|
}
|
|
|
|
type AccessibleRepositoriesByUserOptions struct {
|
|
// Whether to skip counting the total number of repositories.
|
|
SkipCount bool
|
|
}
|
|
|
|
func (db *orgs) AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) {
|
|
conds := db.accessibleRepositoriesByUser(
|
|
db.DB,
|
|
orgID,
|
|
userID,
|
|
accessibleRepositoriesByUserOptions{
|
|
orderBy: "updated_unix DESC",
|
|
page: page,
|
|
pageSize: pageSize,
|
|
},
|
|
).WithContext(ctx)
|
|
|
|
repos := make([]*Repository, 0, pageSize)
|
|
err := conds.Find(&repos).Error
|
|
if err != nil {
|
|
return nil, 0, errors.Wrap(err, "list repositories")
|
|
}
|
|
|
|
if opts.SkipCount {
|
|
return repos, 0, nil
|
|
}
|
|
var count int64
|
|
err = conds.Model(&Repository{}).Count(&count).Error
|
|
if err != nil {
|
|
return nil, 0, errors.Wrap(err, "count repositories")
|
|
}
|
|
return repos, count, nil
|
|
}
|
|
|
|
func (db *orgs) getOrgUser(ctx context.Context, orgID, userID int64) (*OrgUser, error) {
|
|
var ou OrgUser
|
|
return &ou, db.WithContext(ctx).Where("org_id = ? AND uid = ?", orgID, userID).First(&ou).Error
|
|
}
|
|
|
|
func (db *orgs) IsOwnedBy(ctx context.Context, orgID, userID int64) bool {
|
|
ou, err := db.getOrgUser(ctx, orgID, userID)
|
|
return err == nil && ou.IsOwner
|
|
}
|
|
|
|
func (db *orgs) SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error {
|
|
return db.Table("org_user").Where("org_id = ? AND uid = ?", orgID, userID).UpdateColumn("is_public", public).Error
|
|
}
|
|
|
|
func (db *orgs) HasMember(ctx context.Context, orgID, userID int64) (bool, bool) {
|
|
ou, err := db.getOrgUser(ctx, orgID, userID)
|
|
return err == nil, ou != nil && ou.IsPublic
|
|
}
|
|
|
|
type ListOrgMembersOptions struct {
|
|
// The maximum number of members to return.
|
|
Limit int
|
|
}
|
|
|
|
func (db *orgs) ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) {
|
|
/*
|
|
Equivalent SQL for PostgreSQL:
|
|
|
|
SELECT * FROM "user"
|
|
JOIN org_user ON org_user.uid = user.id
|
|
WHERE
|
|
org_user.org_id = @orgID
|
|
ORDER BY user.id ASC
|
|
[LIMIT @limit]
|
|
*/
|
|
conds := db.WithContext(ctx).
|
|
Joins(dbutil.Quote("JOIN org_user ON org_user.uid = %s.id", "user")).
|
|
Where("org_user.org_id = ?", orgID).
|
|
Order(dbutil.Quote("%s.id ASC", "user"))
|
|
if opts.Limit > 0 {
|
|
conds.Limit(opts.Limit)
|
|
}
|
|
var users []*User
|
|
return users, conds.Find(&users).Error
|
|
}
|
|
|
|
type ListOrgsOptions struct {
|
|
// Filter by the membership with the given user ID.
|
|
MemberID int64
|
|
// Whether to include private memberships.
|
|
IncludePrivateMembers bool
|
|
}
|
|
|
|
func (db *orgs) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) {
|
|
if opts.MemberID <= 0 {
|
|
return nil, errors.New("MemberID must be greater than 0")
|
|
}
|
|
|
|
/*
|
|
Equivalent SQL for PostgreSQL:
|
|
|
|
SELECT * FROM "user"
|
|
JOIN org_user ON org_user.org_id = user.id
|
|
WHERE
|
|
org_user.uid = @memberID
|
|
[AND org_user.is_public = @includePrivateMembers]
|
|
ORDER BY user.id ASC
|
|
*/
|
|
conds := db.WithContext(ctx).
|
|
Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")).
|
|
Where("org_user.uid = ?", opts.MemberID).
|
|
Order(dbutil.Quote("%s.id ASC", "user"))
|
|
if !opts.IncludePrivateMembers {
|
|
conds.Where("org_user.is_public = ?", true)
|
|
}
|
|
|
|
var orgs []*Organization
|
|
return orgs, conds.Find(&orgs).Error
|
|
}
|
|
|
|
var _ errutil.NotFound = (*ErrUserNotExist)(nil)
|
|
|
|
type ErrOrganizationNotExist struct {
|
|
args errutil.Args
|
|
}
|
|
|
|
// IsErrOrganizationNotExist returns true if the underlying error has the type
|
|
// ErrOrganizationNotExist.
|
|
func IsErrOrganizationNotExist(err error) bool {
|
|
return errors.As(err, &ErrOrganizationNotExist{})
|
|
}
|
|
|
|
func (err ErrOrganizationNotExist) Error() string {
|
|
return fmt.Sprintf("organization does not exist: %v", err.args)
|
|
}
|
|
|
|
func (ErrOrganizationNotExist) NotFound() bool {
|
|
return true
|
|
}
|
|
|
|
func (db *orgs) GetByName(ctx context.Context, name string) (*Organization, error) {
|
|
org, err := getUserByUsername(ctx, db.DB, UserTypeOrganization, name)
|
|
if err != nil {
|
|
if IsErrUserNotExist(err) {
|
|
return nil, ErrOrganizationNotExist{args: map[string]any{"name": name}}
|
|
}
|
|
return nil, errors.Wrap(err, "get organization by name")
|
|
}
|
|
return org, nil
|
|
}
|
|
|
|
func (db *orgs) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) {
|
|
return searchUserByName(ctx, db.DB, UserTypeOrganization, keyword, page, pageSize, orderBy)
|
|
}
|
|
|
|
func (db *orgs) CountByUser(ctx context.Context, userID int64) (int64, error) {
|
|
var count int64
|
|
return count, db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
|
|
}
|
|
|
|
func (db *orgs) Count(ctx context.Context) int64 {
|
|
var count int64
|
|
db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeOrganization).Count(&count)
|
|
return count
|
|
}
|
|
|
|
var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
|
|
|
|
type ErrTeamNotExist struct {
|
|
args map[string]any
|
|
}
|
|
|
|
func IsErrTeamNotExist(err error) bool {
|
|
return errors.As(err, &ErrTeamNotExist{})
|
|
}
|
|
|
|
func (err ErrTeamNotExist) Error() string {
|
|
return fmt.Sprintf("team does not exist: %v", err.args)
|
|
}
|
|
|
|
func (ErrTeamNotExist) NotFound() bool {
|
|
return true
|
|
}
|
|
|
|
func (db *orgs) GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) {
|
|
var team Team
|
|
err := db.WithContext(ctx).Where("org_id = ? AND lower_name = ?", orgID, strings.ToLower(name)).First(&team).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
|
|
}
|
|
return nil, errors.Wrap(err, "get team by name")
|
|
}
|
|
return &team, nil
|
|
}
|
|
|
|
type Organization = User
|
|
|
|
func (u *Organization) TableName() string {
|
|
return "user"
|
|
}
|
|
|
|
// IsOwnedBy returns true if the given user is an owner of the organization.
|
|
//
|
|
// TODO(unknwon): This is also used in templates, which should be fixed by
|
|
// having a dedicated type `template.Organization`.
|
|
func (u *Organization) IsOwnedBy(userID int64) bool {
|
|
return Orgs.IsOwnedBy(context.TODO(), u.ID, userID)
|
|
}
|
|
|
|
// OrgUser represents relations of organizations and their members.
|
|
type OrgUser struct {
|
|
ID int64 `gorm:"primaryKey"`
|
|
UserID int64 `xorm:"uid INDEX UNIQUE(s)" gorm:"column:uid;uniqueIndex:org_user_user_org_unique;index;not null" json:"Uid"`
|
|
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"`
|
|
}
|