first-commit
This commit is contained in:
21
models/organization/main_test.go
Normal file
21
models/organization/main_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
_ "code.gitea.io/gitea/models/activities"
|
||||
_ "code.gitea.io/gitea/models/organization"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
_ "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
604
models/organization/org.go
Normal file
604
models/organization/org.go
Normal file
@@ -0,0 +1,604 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
|
||||
type ErrOrgNotExist struct {
|
||||
ID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrOrgNotExist checks if an error is a ErrOrgNotExist.
|
||||
func IsErrOrgNotExist(err error) bool {
|
||||
_, ok := err.(ErrOrgNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOrgNotExist) Error() string {
|
||||
return fmt.Sprintf("org does not exist [id: %d, name: %s]", err.ID, err.Name)
|
||||
}
|
||||
|
||||
func (err ErrOrgNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrLastOrgOwner represents a "LastOrgOwner" kind of error.
|
||||
type ErrLastOrgOwner struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrLastOrgOwner checks if an error is a ErrLastOrgOwner.
|
||||
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)
|
||||
}
|
||||
|
||||
// ErrUserNotAllowedCreateOrg represents a "UserNotAllowedCreateOrg" kind of error.
|
||||
type ErrUserNotAllowedCreateOrg struct{}
|
||||
|
||||
// IsErrUserNotAllowedCreateOrg checks if an error is an ErrUserNotAllowedCreateOrg.
|
||||
func IsErrUserNotAllowedCreateOrg(err error) bool {
|
||||
_, ok := err.(ErrUserNotAllowedCreateOrg)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserNotAllowedCreateOrg) Error() string {
|
||||
return "user is not allowed to create organizations"
|
||||
}
|
||||
|
||||
func (err ErrUserNotAllowedCreateOrg) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Organization represents an organization
|
||||
type Organization user_model.User
|
||||
|
||||
// OrgFromUser converts user to organization
|
||||
func OrgFromUser(user *user_model.User) *Organization {
|
||||
return (*Organization)(user)
|
||||
}
|
||||
|
||||
// TableName represents the real table name of Organization
|
||||
func (Organization) TableName() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
// IsOwnedBy returns true if given user is in the owner team.
|
||||
func (org *Organization) IsOwnedBy(ctx context.Context, uid int64) (bool, error) {
|
||||
return IsOrganizationOwner(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// IsOrgAdmin returns true if given user is in the owner team or an admin team.
|
||||
func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error) {
|
||||
return IsOrganizationAdmin(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// IsOrgMember returns true if given user is member of organization.
|
||||
func (org *Organization) IsOrgMember(ctx context.Context, uid int64) (bool, error) {
|
||||
return IsOrganizationMember(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// CanCreateOrgRepo returns true if given user can create repo in organization
|
||||
func (org *Organization) CanCreateOrgRepo(ctx context.Context, uid int64) (bool, error) {
|
||||
return CanCreateOrgRepo(ctx, org.ID, uid)
|
||||
}
|
||||
|
||||
// GetTeam returns named team of organization.
|
||||
func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
|
||||
return GetTeam(ctx, org.ID, name)
|
||||
}
|
||||
|
||||
// GetOwnerTeam returns owner team of organization.
|
||||
func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
|
||||
return org.GetTeam(ctx, OwnerTeamName)
|
||||
}
|
||||
|
||||
// FindOrgTeams returns all teams of a given organization
|
||||
func FindOrgTeams(ctx context.Context, orgID int64) ([]*Team, error) {
|
||||
var teams []*Team
|
||||
return teams, db.GetEngine(ctx).
|
||||
Where("org_id=?", orgID).
|
||||
OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// LoadTeams load teams if not loaded.
|
||||
func (org *Organization) LoadTeams(ctx context.Context) ([]*Team, error) {
|
||||
return FindOrgTeams(ctx, org.ID)
|
||||
}
|
||||
|
||||
// GetMembers returns all members of organization.
|
||||
func (org *Organization) GetMembers(ctx context.Context, doer *user_model.User) (user_model.UserList, map[int64]bool, error) {
|
||||
return FindOrgMembers(ctx, &FindOrgMembersOpts{
|
||||
Doer: doer,
|
||||
OrgID: org.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// HasMemberWithUserID returns true if user with userID is part of the u organisation.
|
||||
func (org *Organization) HasMemberWithUserID(ctx context.Context, userID int64) bool {
|
||||
return org.hasMemberWithUserID(ctx, userID)
|
||||
}
|
||||
|
||||
func (org *Organization) hasMemberWithUserID(ctx context.Context, userID int64) bool {
|
||||
isMember, err := IsOrganizationMember(ctx, org.ID, userID)
|
||||
if err != nil {
|
||||
log.Error("IsOrganizationMember: %v", err)
|
||||
return false
|
||||
}
|
||||
return isMember
|
||||
}
|
||||
|
||||
// AvatarLink returns the full avatar link with http host
|
||||
func (org *Organization) AvatarLink(ctx context.Context) string {
|
||||
return org.AsUser().AvatarLink(ctx)
|
||||
}
|
||||
|
||||
// HTMLURL returns the organization's full link.
|
||||
func (org *Organization) HTMLURL() string {
|
||||
return org.AsUser().HTMLURL()
|
||||
}
|
||||
|
||||
// OrganisationLink returns the organization sub page link.
|
||||
func (org *Organization) OrganisationLink() string {
|
||||
return org.AsUser().OrganisationLink()
|
||||
}
|
||||
|
||||
// ShortName ellipses username to length
|
||||
func (org *Organization) ShortName(length int) string {
|
||||
return org.AsUser().ShortName(length)
|
||||
}
|
||||
|
||||
// HomeLink returns the user or organization home page link.
|
||||
func (org *Organization) HomeLink() string {
|
||||
return org.AsUser().HomeLink()
|
||||
}
|
||||
|
||||
// FindOrgMembersOpts represensts find org members conditions
|
||||
type FindOrgMembersOpts struct {
|
||||
db.ListOptions
|
||||
Doer *user_model.User
|
||||
IsDoerMember bool
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
func (opts FindOrgMembersOpts) PublicOnly() bool {
|
||||
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
|
||||
}
|
||||
|
||||
// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
|
||||
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess *xorm.Session) {
|
||||
if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
|
||||
teamMates := builder.Select("DISTINCT team_user.uid").
|
||||
From("team_user").
|
||||
Where(builder.In("team_user.team_id", getUserTeamIDsQueryBuilder(opts.OrgID, opts.Doer.ID))).
|
||||
And(builder.Eq{"team_user.org_id": opts.OrgID})
|
||||
|
||||
sess.And(
|
||||
builder.In("org_user.uid", teamMates).
|
||||
Or(builder.Eq{"org_user.is_public": true}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// CountOrgMembers counts the organization's members
|
||||
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
|
||||
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
||||
if opts.PublicOnly() {
|
||||
sess = sess.And("is_public = ?", true)
|
||||
} else {
|
||||
opts.applyTeamMatesOnlyFilter(sess)
|
||||
}
|
||||
|
||||
return sess.Count(new(OrgUser))
|
||||
}
|
||||
|
||||
// FindOrgMembers loads organization members according conditions
|
||||
func FindOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (user_model.UserList, map[int64]bool, error) {
|
||||
ous, err := GetOrgUsersByOrgID(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ids := make([]int64, len(ous))
|
||||
idsIsPublic := make(map[int64]bool, len(ous))
|
||||
for i, ou := range ous {
|
||||
ids[i] = ou.UID
|
||||
idsIsPublic[ou.UID] = ou.IsPublic
|
||||
}
|
||||
|
||||
users, err := user_model.GetUsersByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return users, idsIsPublic, nil
|
||||
}
|
||||
|
||||
// AsUser returns the org as user object
|
||||
func (org *Organization) AsUser() *user_model.User {
|
||||
return (*user_model.User)(org)
|
||||
}
|
||||
|
||||
// DisplayName returns full name if it's not empty,
|
||||
// returns username otherwise.
|
||||
func (org *Organization) DisplayName() string {
|
||||
return org.AsUser().DisplayName()
|
||||
}
|
||||
|
||||
// CustomAvatarRelativePath returns user custom avatar relative path.
|
||||
func (org *Organization) CustomAvatarRelativePath() string {
|
||||
return org.Avatar
|
||||
}
|
||||
|
||||
// UnitPermission returns unit permission
|
||||
func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
|
||||
if doer != nil {
|
||||
teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
log.Error("GetUserOrgTeams: %v", err)
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
|
||||
if err := teams.LoadUnits(ctx); err != nil {
|
||||
log.Error("LoadUnits: %v", err)
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
|
||||
if len(teams) > 0 {
|
||||
return teams.UnitMaxAccess(unitType)
|
||||
}
|
||||
}
|
||||
|
||||
if org.Visibility.IsPublic() {
|
||||
return perm.AccessModeRead
|
||||
}
|
||||
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
|
||||
// CreateOrganization creates record of a new organization.
|
||||
func CreateOrganization(ctx context.Context, org *Organization, owner *user_model.User) (err error) {
|
||||
if !owner.CanCreateOrganization() {
|
||||
return ErrUserNotAllowedCreateOrg{}
|
||||
}
|
||||
|
||||
if err = user_model.IsUsableUsername(org.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isExist, err := user_model.IsUserExist(ctx, 0, org.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if isExist {
|
||||
return user_model.ErrUserAlreadyExist{Name: org.Name}
|
||||
}
|
||||
|
||||
org.LowerName = strings.ToLower(org.Name)
|
||||
if org.Rands, err = user_model.GetUserSalt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if org.Salt, err = user_model.GetUserSalt(); err != nil {
|
||||
return err
|
||||
}
|
||||
org.UseCustomAvatar = true
|
||||
org.MaxRepoCreation = -1
|
||||
org.NumTeams = 1
|
||||
org.NumMembers = 1
|
||||
org.Type = user_model.UserTypeOrganization
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = user_model.DeleteUserRedirect(ctx, org.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, org); err != nil {
|
||||
return fmt.Errorf("insert organization: %w", err)
|
||||
}
|
||||
if err = user_model.GenerateRandomAvatar(ctx, org.AsUser()); err != nil {
|
||||
return fmt.Errorf("generate random avatar: %w", err)
|
||||
}
|
||||
|
||||
// Add initial creator to organization and owner team.
|
||||
if err = db.Insert(ctx, &OrgUser{
|
||||
UID: owner.ID,
|
||||
OrgID: org.ID,
|
||||
IsPublic: setting.Service.DefaultOrgMemberVisible,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("insert org-user relation: %w", err)
|
||||
}
|
||||
|
||||
// Create default owner team.
|
||||
t := &Team{
|
||||
OrgID: org.ID,
|
||||
LowerName: strings.ToLower(OwnerTeamName),
|
||||
Name: OwnerTeamName,
|
||||
AccessMode: perm.AccessModeOwner,
|
||||
NumMembers: 1,
|
||||
IncludesAllRepositories: true,
|
||||
CanCreateOrgRepo: true,
|
||||
}
|
||||
if err = db.Insert(ctx, t); err != nil {
|
||||
return fmt.Errorf("insert owner team: %w", err)
|
||||
}
|
||||
|
||||
// insert units for team
|
||||
units := make([]TeamUnit, 0, len(unit.AllRepoUnitTypes))
|
||||
for _, tp := range unit.AllRepoUnitTypes {
|
||||
up := perm.AccessModeOwner
|
||||
if tp == unit.TypeExternalTracker || tp == unit.TypeExternalWiki {
|
||||
up = perm.AccessModeRead
|
||||
}
|
||||
units = append(units, TeamUnit{
|
||||
OrgID: org.ID,
|
||||
TeamID: t.ID,
|
||||
Type: tp,
|
||||
AccessMode: up,
|
||||
})
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, &units); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, &TeamUser{
|
||||
UID: owner.ID,
|
||||
OrgID: org.ID,
|
||||
TeamID: t.ID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("insert team-user relation: %w", err)
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// GetOrgByName returns organization by given name.
|
||||
func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrOrgNotExist{0, name}
|
||||
}
|
||||
u := &Organization{
|
||||
LowerName: strings.ToLower(name),
|
||||
Type: user_model.UserTypeOrganization,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgNotExist{0, name}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetOrgUserMaxAuthorizeLevel returns highest authorize level of user in an organization
|
||||
func (org *Organization) GetOrgUserMaxAuthorizeLevel(ctx context.Context, uid int64) (perm.AccessMode, error) {
|
||||
var authorize perm.AccessMode
|
||||
_, err := db.GetEngine(ctx).
|
||||
Select("max(team.authorize)").
|
||||
Table("team").
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team_user.uid = ?", uid).
|
||||
And("team_user.org_id = ?", org.ID).
|
||||
Get(&authorize)
|
||||
return authorize, err
|
||||
}
|
||||
|
||||
// GetUsersWhoCanCreateOrgRepo returns users which are able to create repo in organization
|
||||
func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*user_model.User, error) {
|
||||
// Use a map, in order to de-duplicate users.
|
||||
users := make(map[int64]*user_model.User)
|
||||
return users, db.GetEngine(ctx).
|
||||
Join("INNER", "`team_user`", "`team_user`.uid=`user`.id").
|
||||
Join("INNER", "`team`", "`team`.id=`team_user`.team_id").
|
||||
Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})).
|
||||
And("team_user.org_id = ?", orgID).Find(&users)
|
||||
}
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
||||
// If user is nil, it's an anonymous user/request.
|
||||
// The Ghost user is handled like an anonymous user.
|
||||
if user == nil || user.IsGhost() {
|
||||
return orgOrUser.Visibility == structs.VisibleTypePublic
|
||||
}
|
||||
|
||||
if user.IsAdmin || orgOrUser.ID == user.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasOrgsVisible tells if the given user can see at least one of the orgs provided
|
||||
func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.User) bool {
|
||||
if len(orgs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
if HasOrgOrUserVisible(ctx, org.AsUser(), user) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
|
||||
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
|
||||
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
||||
if opts.PublicOnly() {
|
||||
sess = sess.And("is_public = ?", true)
|
||||
} else {
|
||||
opts.applyTeamMatesOnlyFilter(sess)
|
||||
}
|
||||
|
||||
if opts.ListOptions.PageSize > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
|
||||
ous := make([]*OrgUser, 0, opts.PageSize)
|
||||
return ous, sess.Find(&ous)
|
||||
}
|
||||
|
||||
var ous []*OrgUser
|
||||
return ous, sess.Find(&ous)
|
||||
}
|
||||
|
||||
// ChangeOrgUserStatus changes public or private membership status.
|
||||
func ChangeOrgUserStatus(ctx context.Context, orgID, uid int64, public bool) error {
|
||||
ou := new(OrgUser)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("uid=?", uid).
|
||||
And("org_id=?", orgID).
|
||||
Get(ou)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
ou.IsPublic = public
|
||||
_, err = db.GetEngine(ctx).ID(ou.ID).Cols("is_public").Update(ou)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddOrgUser adds new user to given organization.
|
||||
func AddOrgUser(ctx context.Context, orgID, uid int64) error {
|
||||
isAlreadyMember, err := IsOrganizationMember(ctx, orgID, uid)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// check in transaction
|
||||
isAlreadyMember, err = IsOrganizationMember(ctx, orgID, uid)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
||||
ou := &OrgUser{
|
||||
UID: uid,
|
||||
OrgID: orgID,
|
||||
IsPublic: setting.Service.DefaultOrgMemberVisible,
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, ou); err != nil {
|
||||
return err
|
||||
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// GetOrgByID returns the user object by given ID if exists.
|
||||
func GetOrgByID(ctx context.Context, id int64) (*Organization, error) {
|
||||
u := new(Organization)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, user_model.ErrUserNotExist{
|
||||
UID: id,
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// RemoveOrgRepo removes all team-repository relations of organization.
|
||||
func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error {
|
||||
teamRepos := make([]*TeamRepo, 0, 10)
|
||||
e := db.GetEngine(ctx)
|
||||
if err := e.Find(&teamRepos, &TeamRepo{OrgID: orgID, RepoID: repoID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(teamRepos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := e.Delete(&TeamRepo{
|
||||
OrgID: orgID,
|
||||
RepoID: repoID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
teamIDs := make([]int64, len(teamRepos))
|
||||
for i, teamRepo := range teamRepos {
|
||||
teamIDs[i] = teamRepo.TeamID
|
||||
}
|
||||
|
||||
_, err := e.Decr("num_repos").In("id", teamIDs).Update(new(Team))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserTeams returns all teams that belong to user,
|
||||
// and that the user has joined.
|
||||
func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, org.NumTeams)
|
||||
return teams, db.GetEngine(ctx).
|
||||
Where("`team_user`.org_id = ?", org.ID).
|
||||
Join("INNER", "team_user", "`team_user`.team_id = team.id").
|
||||
Join("INNER", "`user`", "`user`.id=team_user.uid").
|
||||
And("`team_user`.uid = ?", userID).
|
||||
Asc("`user`.name").
|
||||
Cols(cols...).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
|
||||
func (org *Organization) GetUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) {
|
||||
teamIDs := make([]int64, 0, org.NumTeams)
|
||||
return teamIDs, db.GetEngine(ctx).
|
||||
Table("team").
|
||||
Cols("team.id").
|
||||
Where("`team_user`.org_id = ?", org.ID).
|
||||
Join("INNER", "team_user", "`team_user`.team_id = team.id").
|
||||
And("`team_user`.uid = ?", userID).
|
||||
Find(&teamIDs)
|
||||
}
|
||||
|
||||
func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
||||
return builder.Select("team.id").From("team").
|
||||
InnerJoin("team_user", "team_user.team_id = team.id").
|
||||
Where(builder.Eq{
|
||||
"team_user.org_id": orgID,
|
||||
"team_user.uid": userID,
|
||||
})
|
||||
}
|
173
models/organization/org_list.go
Normal file
173
models/organization/org_list.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type OrgList []*Organization
|
||||
|
||||
func (orgs OrgList) LoadTeams(ctx context.Context) (map[int64]TeamList, error) {
|
||||
if len(orgs) == 0 {
|
||||
return map[int64]TeamList{}, nil
|
||||
}
|
||||
|
||||
orgIDs := make([]int64, len(orgs))
|
||||
for i, org := range orgs {
|
||||
orgIDs[i] = org.ID
|
||||
}
|
||||
|
||||
teams, err := GetTeamsByOrgIDs(ctx, orgIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamMap := make(map[int64]TeamList, len(orgs))
|
||||
for _, team := range teams {
|
||||
teamMap[team.OrgID] = append(teamMap[team.OrgID], team)
|
||||
}
|
||||
|
||||
return teamMap, nil
|
||||
}
|
||||
|
||||
// SearchOrganizationsOptions options to filter organizations
|
||||
type SearchOrganizationsOptions struct {
|
||||
db.ListOptions
|
||||
All bool
|
||||
}
|
||||
|
||||
// FindOrgOptions finds orgs options
|
||||
type FindOrgOptions struct {
|
||||
db.ListOptions
|
||||
UserID int64
|
||||
IncludeVisibility structs.VisibleType
|
||||
}
|
||||
|
||||
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
||||
cond := builder.Eq{"uid": userID}
|
||||
if !includePrivate {
|
||||
cond["is_public"] = true
|
||||
}
|
||||
return builder.Select("org_id").From("org_user").Where(cond)
|
||||
}
|
||||
|
||||
func (opts FindOrgOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludeVisibility == structs.VisibleTypePrivate)))
|
||||
}
|
||||
// public=0, limited=1, private=2
|
||||
cond = cond.And(builder.Lte{"`user`.visibility": opts.IncludeVisibility})
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindOrgOptions) ToOrders() string {
|
||||
return "`user`.lower_name ASC"
|
||||
}
|
||||
|
||||
func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType {
|
||||
if doer == nil || other == nil {
|
||||
return structs.VisibleTypePublic
|
||||
}
|
||||
if doer.IsAdmin || doer.ID == other.ID {
|
||||
return structs.VisibleTypePrivate
|
||||
}
|
||||
return structs.VisibleTypeLimited
|
||||
}
|
||||
|
||||
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
|
||||
// are allowed to create repos.
|
||||
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
|
||||
orgs := make([]*Organization, 0, 10)
|
||||
|
||||
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
|
||||
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
|
||||
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
|
||||
Where(builder.Eq{"`team_user`.uid": userID}).
|
||||
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
|
||||
Asc("`user`.name").
|
||||
Find(&orgs)
|
||||
}
|
||||
|
||||
// MinimalOrg represents a simple organization with only the needed columns
|
||||
type MinimalOrg = Organization
|
||||
|
||||
// GetUserOrgsList returns all organizations the given user has access to
|
||||
func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) {
|
||||
schema, err := db.TableInfo(new(user_model.User))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outputCols := []string{
|
||||
"id",
|
||||
"name",
|
||||
"full_name",
|
||||
"visibility",
|
||||
"avatar",
|
||||
"avatar_email",
|
||||
"use_custom_avatar",
|
||||
}
|
||||
|
||||
selectColumns := &strings.Builder{}
|
||||
for i, col := range outputCols {
|
||||
fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col)
|
||||
if i < len(outputCols)-1 {
|
||||
selectColumns.WriteString(", ")
|
||||
}
|
||||
}
|
||||
columnsStr := selectColumns.String()
|
||||
|
||||
var orgs []*MinimalOrg
|
||||
if err := db.GetEngine(ctx).Select(columnsStr).
|
||||
Table("user").
|
||||
Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))).
|
||||
OrderBy("`user`.lower_name ASC").
|
||||
Find(&orgs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type orgCount struct {
|
||||
OrgID int64
|
||||
RepoCount int
|
||||
}
|
||||
var orgCounts []orgCount
|
||||
if err := db.GetEngine(ctx).
|
||||
Select("owner_id AS org_id, COUNT(DISTINCT(repository.id)) as repo_count").
|
||||
Table("repository").
|
||||
Join("INNER", "org_user", "owner_id = org_user.org_id").
|
||||
Where("org_user.uid = ?", user.ID).
|
||||
And(builder.Or(
|
||||
builder.Eq{"repository.is_private": false},
|
||||
builder.In("repository.id", builder.Select("repo_id").From("team_repo").
|
||||
InnerJoin("team_user", "team_user.team_id = team_repo.team_id").
|
||||
Where(builder.Eq{"team_user.uid": user.ID})),
|
||||
builder.In("repository.id", builder.Select("repo_id").From("collaboration").
|
||||
Where(builder.Eq{"user_id": user.ID})),
|
||||
)).
|
||||
GroupBy("owner_id").Find(&orgCounts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgCountMap := make(map[int64]int, len(orgCounts))
|
||||
for _, orgCount := range orgCounts {
|
||||
orgCountMap[orgCount.OrgID] = orgCount.RepoCount
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
org.NumRepos = orgCountMap[org.ID]
|
||||
}
|
||||
|
||||
return orgs, nil
|
||||
}
|
84
models/organization/org_list_test.go
Normal file
84
models/organization/org_list_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOrgList(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("CountOrganizations", testCountOrganizations)
|
||||
t.Run("FindOrgs", testFindOrgs)
|
||||
t.Run("GetUserOrgsList", testGetUserOrgsList)
|
||||
t.Run("LoadOrgListTeams", testLoadOrgListTeams)
|
||||
t.Run("DoerViewOtherVisibility", testDoerViewOtherVisibility)
|
||||
}
|
||||
|
||||
func testCountOrganizations(t *testing.T) {
|
||||
expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
|
||||
assert.NoError(t, err)
|
||||
cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludeVisibility: structs.VisibleTypePrivate})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, cnt)
|
||||
}
|
||||
|
||||
func testFindOrgs(t *testing.T) {
|
||||
orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||
UserID: 4,
|
||||
IncludeVisibility: structs.VisibleTypePrivate,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, orgs, 1) {
|
||||
assert.EqualValues(t, 3, orgs[0].ID)
|
||||
}
|
||||
|
||||
orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||
UserID: 4,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, orgs)
|
||||
|
||||
total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||
UserID: 4,
|
||||
IncludeVisibility: structs.VisibleTypePrivate,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
}
|
||||
|
||||
func testGetUserOrgsList(t *testing.T) {
|
||||
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, orgs, 1) {
|
||||
assert.EqualValues(t, 3, orgs[0].ID)
|
||||
// repo_id: 3 is in the team, 32 is public, 5 is private with no team
|
||||
assert.Equal(t, 2, orgs[0].NumRepos)
|
||||
}
|
||||
}
|
||||
|
||||
func testLoadOrgListTeams(t *testing.T) {
|
||||
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, orgs, 1)
|
||||
teamsMap, err := organization.OrgList(orgs).LoadTeams(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teamsMap, 1)
|
||||
assert.Len(t, teamsMap[3], 5)
|
||||
}
|
||||
|
||||
func testDoerViewOtherVisibility(t *testing.T) {
|
||||
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil))
|
||||
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2}))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1}))
|
||||
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2}))
|
||||
}
|
534
models/organization/org_test.go
Normal file
534
models/organization/org_test.go
Normal file
@@ -0,0 +1,534 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser_IsOwnedBy(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
for _, testCase := range []struct {
|
||||
OrgID int64
|
||||
UserID int64
|
||||
ExpectedOwner bool
|
||||
}{
|
||||
{3, 2, true},
|
||||
{3, 1, false},
|
||||
{3, 3, false},
|
||||
{3, 4, false},
|
||||
{2, 2, false}, // user2 is not an organization
|
||||
{2, 3, false},
|
||||
} {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: testCase.OrgID})
|
||||
isOwner, err := org.IsOwnedBy(db.DefaultContext, testCase.UserID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testCase.ExpectedOwner, isOwner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_IsOrgMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
for _, testCase := range []struct {
|
||||
OrgID int64
|
||||
UserID int64
|
||||
ExpectedMember bool
|
||||
}{
|
||||
{3, 2, true},
|
||||
{3, 4, true},
|
||||
{3, 1, false},
|
||||
{3, 3, false},
|
||||
{2, 2, false}, // user2 is not an organization
|
||||
{2, 3, false},
|
||||
} {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: testCase.OrgID})
|
||||
isMember, err := org.IsOrgMember(db.DefaultContext, testCase.UserID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testCase.ExpectedMember, isMember)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_GetTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
team, err := org.GetTeam(db.DefaultContext, "team1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, org.ID, team.OrgID)
|
||||
assert.Equal(t, "team1", team.LowerName)
|
||||
|
||||
_, err = org.GetTeam(db.DefaultContext, "does not exist")
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
|
||||
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
|
||||
_, err = nonOrg.GetTeam(db.DefaultContext, "team")
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
}
|
||||
|
||||
func TestUser_GetOwnerTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
team, err := org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, org.ID, team.OrgID)
|
||||
|
||||
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
|
||||
_, err = nonOrg.GetOwnerTeam(db.DefaultContext)
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
}
|
||||
|
||||
func TestUser_GetTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
teams, err := org.LoadTeams(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 5) {
|
||||
assert.Equal(t, int64(1), teams[0].ID)
|
||||
assert.Equal(t, int64(2), teams[1].ID)
|
||||
assert.Equal(t, int64(12), teams[2].ID)
|
||||
assert.Equal(t, int64(14), teams[3].ID)
|
||||
assert.Equal(t, int64(7), teams[4].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_GetMembers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, members, 3) {
|
||||
assert.Equal(t, int64(2), members[0].ID)
|
||||
assert.Equal(t, int64(28), members[1].ID)
|
||||
assert.Equal(t, int64(4), members[2].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrgByName(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org, err := organization.GetOrgByName(db.DefaultContext, "org3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, org.ID)
|
||||
assert.Equal(t, "org3", org.Name)
|
||||
|
||||
_, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual
|
||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||
|
||||
_, err = organization.GetOrgByName(db.DefaultContext, "") // corner case
|
||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||
}
|
||||
|
||||
func TestIsOrganizationOwner(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(orgID, userID int64, expected bool) {
|
||||
isOwner, err := organization.IsOrganizationOwner(db.DefaultContext, orgID, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, isOwner)
|
||||
}
|
||||
test(3, 2, true)
|
||||
test(3, 3, false)
|
||||
test(6, 5, true)
|
||||
test(6, 4, false)
|
||||
test(unittest.NonexistentID, unittest.NonexistentID, false)
|
||||
}
|
||||
|
||||
func TestIsOrganizationMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(orgID, userID int64, expected bool) {
|
||||
isMember, err := organization.IsOrganizationMember(db.DefaultContext, orgID, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, isMember)
|
||||
}
|
||||
test(3, 2, true)
|
||||
test(3, 3, false)
|
||||
test(3, 4, true)
|
||||
test(6, 5, true)
|
||||
test(6, 4, false)
|
||||
test(unittest.NonexistentID, unittest.NonexistentID, false)
|
||||
}
|
||||
|
||||
func TestIsPublicMembership(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(orgID, userID int64, expected bool) {
|
||||
isMember, err := organization.IsPublicMembership(db.DefaultContext, orgID, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, isMember)
|
||||
}
|
||||
test(3, 2, true)
|
||||
test(3, 3, false)
|
||||
test(3, 4, false)
|
||||
test(6, 5, true)
|
||||
test(6, 4, false)
|
||||
test(unittest.NonexistentID, unittest.NonexistentID, false)
|
||||
}
|
||||
|
||||
func TestRestrictedUserOrgMembers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
ID: 29,
|
||||
IsRestricted: true,
|
||||
})
|
||||
// ensure fixtures return restricted user
|
||||
require.True(t, restrictedUser.IsRestricted)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts *organization.FindOrgMembersOpts
|
||||
expectedUIDs []int64
|
||||
}{
|
||||
{
|
||||
name: "restricted user sees public members and teammates",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 17, // org17 where user29 is in team9
|
||||
Doer: restrictedUser,
|
||||
IsDoerMember: true,
|
||||
},
|
||||
expectedUIDs: []int64{2, 15, 20, 29}, // Public members (2) + teammates in team9 (15, 20, 29)
|
||||
},
|
||||
{
|
||||
name: "restricted user sees only public members when not member",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3, // org3 where user29 is not a member
|
||||
Doer: restrictedUser,
|
||||
},
|
||||
expectedUIDs: []int64{2, 28}, // Only public members
|
||||
},
|
||||
{
|
||||
name: "non logged in only shows public members",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 3,
|
||||
},
|
||||
expectedUIDs: []int64{2, 28}, // Only public members
|
||||
},
|
||||
{
|
||||
name: "non restricted user sees all members",
|
||||
opts: &organization.FindOrgMembersOpts{
|
||||
OrgID: 17,
|
||||
Doer: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}),
|
||||
IsDoerMember: true,
|
||||
},
|
||||
expectedUIDs: []int64{2, 15, 18, 20, 29}, // All members
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
count, err := organization.CountOrgMembers(db.DefaultContext, tc.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, len(tc.expectedUIDs), count)
|
||||
|
||||
members, err := organization.GetOrgUsersByOrgID(db.DefaultContext, tc.opts)
|
||||
assert.NoError(t, err)
|
||||
memberUIDs := make([]int64, 0, len(members))
|
||||
for _, member := range members {
|
||||
memberUIDs = append(memberUIDs, member.UID)
|
||||
}
|
||||
slices.Sort(memberUIDs)
|
||||
assert.Equal(t, tc.expectedUIDs, memberUIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrgUsersByOrgID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
Doer: &user_model.User{IsAdmin: true},
|
||||
OrgID: 3,
|
||||
}
|
||||
assert.False(t, opts.PublicOnly())
|
||||
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
|
||||
assert.NoError(t, err)
|
||||
sort.Slice(orgUsers, func(i, j int) bool {
|
||||
return orgUsers[i].ID < orgUsers[j].ID
|
||||
})
|
||||
assert.Equal(t, []*organization.OrgUser{{
|
||||
ID: 1,
|
||||
OrgID: 3,
|
||||
UID: 2,
|
||||
IsPublic: true,
|
||||
}, {
|
||||
ID: 2,
|
||||
OrgID: 3,
|
||||
UID: 4,
|
||||
IsPublic: false,
|
||||
}, {
|
||||
ID: 9,
|
||||
OrgID: 3,
|
||||
UID: 28,
|
||||
IsPublic: true,
|
||||
}}, orgUsers)
|
||||
|
||||
opts = &organization.FindOrgMembersOpts{OrgID: 3}
|
||||
assert.True(t, opts.PublicOnly())
|
||||
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, orgUsers, 2)
|
||||
|
||||
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
|
||||
ListOptions: db.ListOptions{},
|
||||
OrgID: unittest.NonexistentID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, orgUsers)
|
||||
}
|
||||
|
||||
func TestChangeOrgUserStatus(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(orgID, userID int64, public bool) {
|
||||
assert.NoError(t, organization.ChangeOrgUserStatus(db.DefaultContext, orgID, userID, public))
|
||||
orgUser := unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
|
||||
assert.Equal(t, public, orgUser.IsPublic)
|
||||
}
|
||||
|
||||
testSuccess(3, 2, false)
|
||||
testSuccess(3, 2, false)
|
||||
testSuccess(3, 4, true)
|
||||
assert.NoError(t, organization.ChangeOrgUserStatus(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID, true))
|
||||
}
|
||||
|
||||
func TestUser_GetUserTeamIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID int64, expected []int64) {
|
||||
teamIDs, err := org.GetUserTeamIDs(db.DefaultContext, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, teamIDs)
|
||||
}
|
||||
testSuccess(2, []int64{1, 2, 14})
|
||||
testSuccess(4, []int64{2})
|
||||
testSuccess(unittest.NonexistentID, []int64{})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_CountRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID, expectedCount int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
count, err := env.CountRepos(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedCount, count)
|
||||
}
|
||||
testSuccess(2, 3)
|
||||
testSuccess(4, 2)
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repoIDs, err := env.RepoIDs(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRepoIDs, repoIDs)
|
||||
}
|
||||
testSuccess(2, []int64{3, 5, 32})
|
||||
testSuccess(4, []int64{3, 32})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repos, err := env.MirrorRepos(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
|
||||
for i, repoID := range expectedRepoIDs {
|
||||
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
|
||||
&repo_model.Repository{ID: repoID})
|
||||
}
|
||||
assert.Equal(t, expectedRepos, repos)
|
||||
}
|
||||
testSuccess(2, []int64{5})
|
||||
testSuccess(4, []int64{})
|
||||
}
|
||||
|
||||
func TestHasOrgVisibleTypePublic(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
|
||||
const newOrgName = "test-org-public"
|
||||
org := &organization.Organization{
|
||||
Name: newOrgName,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner))
|
||||
org = unittest.AssertExistsAndLoadBean(t,
|
||||
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner)
|
||||
test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3)
|
||||
test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil)
|
||||
assert.True(t, test1) // owner of org
|
||||
assert.True(t, test2) // user not a part of org
|
||||
assert.True(t, test3) // logged out user
|
||||
}
|
||||
|
||||
func TestHasOrgVisibleTypeLimited(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
|
||||
const newOrgName = "test-org-limited"
|
||||
org := &organization.Organization{
|
||||
Name: newOrgName,
|
||||
Visibility: structs.VisibleTypeLimited,
|
||||
}
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner))
|
||||
org = unittest.AssertExistsAndLoadBean(t,
|
||||
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner)
|
||||
test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3)
|
||||
test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil)
|
||||
assert.True(t, test1) // owner of org
|
||||
assert.True(t, test2) // user not a part of org
|
||||
assert.False(t, test3) // logged out user
|
||||
}
|
||||
|
||||
func TestHasOrgVisibleTypePrivate(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
|
||||
const newOrgName = "test-org-private"
|
||||
org := &organization.Organization{
|
||||
Name: newOrgName,
|
||||
Visibility: structs.VisibleTypePrivate,
|
||||
}
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner))
|
||||
org = unittest.AssertExistsAndLoadBean(t,
|
||||
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
|
||||
test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner)
|
||||
test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3)
|
||||
test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil)
|
||||
assert.True(t, test1) // owner of org
|
||||
assert.False(t, test2) // user not a part of org
|
||||
assert.False(t, test3) // logged out user
|
||||
}
|
||||
|
||||
func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
users, err := organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 2)
|
||||
var ids []int64
|
||||
for i := range users {
|
||||
ids = append(ids, users[i].ID)
|
||||
}
|
||||
assert.ElementsMatch(t, ids, []int64{2, 28})
|
||||
|
||||
users, err = organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 1)
|
||||
assert.NotNil(t, users[5])
|
||||
}
|
||||
|
||||
func TestUser_RemoveOrgRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: org.ID})
|
||||
|
||||
// remove a repo that does belong to org
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID})
|
||||
assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, repo.ID))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) // repo should still exist
|
||||
|
||||
// remove a repo that does not belong to org
|
||||
assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, repo.ID))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID})
|
||||
|
||||
assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, unittest.NonexistentID))
|
||||
|
||||
unittest.CheckConsistencyFor(t,
|
||||
&user_model.User{ID: org.ID},
|
||||
&organization.Team{OrgID: org.ID},
|
||||
&repo_model.Repository{ID: repo.ID})
|
||||
}
|
||||
|
||||
func TestCreateOrganization(t *testing.T) {
|
||||
// successful creation of org
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
const newOrgName = "neworg"
|
||||
org := &organization.Organization{
|
||||
Name: newOrgName,
|
||||
}
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: newOrgName, Type: user_model.UserTypeOrganization})
|
||||
assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner))
|
||||
org = unittest.AssertExistsAndLoadBean(t,
|
||||
&organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization})
|
||||
ownerTeam := unittest.AssertExistsAndLoadBean(t,
|
||||
&organization.Team{Name: organization.OwnerTeamName, OrgID: org.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: owner.ID, TeamID: ownerTeam.ID})
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestCreateOrganization2(t *testing.T) {
|
||||
// unauthorized creation of org
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
const newOrgName = "neworg"
|
||||
org := &organization.Organization{
|
||||
Name: newOrgName,
|
||||
}
|
||||
|
||||
unittest.AssertNotExistsBean(t, &organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization})
|
||||
err := organization.CreateOrganization(db.DefaultContext, org, owner)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, organization.IsErrUserNotAllowedCreateOrg(err))
|
||||
unittest.AssertNotExistsBean(t, &organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization})
|
||||
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestCreateOrganization3(t *testing.T) {
|
||||
// create org with same name as existent org
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org := &organization.Organization{Name: "org3"} // should already exist
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: org.Name}) // sanity check
|
||||
err := organization.CreateOrganization(db.DefaultContext, org, owner)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user_model.IsErrUserAlreadyExist(err))
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
||||
|
||||
func TestCreateOrganization4(t *testing.T) {
|
||||
// create org with unusable name
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
err := organization.CreateOrganization(db.DefaultContext, &organization.Organization{Name: "assets"}, owner)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, db.IsErrNameReserved(err))
|
||||
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
|
||||
}
|
198
models/organization/org_user.go
Normal file
198
models/organization/org_user.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ________ ____ ___
|
||||
// \_____ \_______ ____ | | \______ ___________
|
||||
// / | \_ __ \/ ___\| | / ___// __ \_ __ \
|
||||
// / | \ | \/ /_/ > | /\___ \\ ___/| | \/
|
||||
// \_______ /__| \___ /|______//____ >\___ >__|
|
||||
// \/ /_____/ \/ \/
|
||||
|
||||
// OrgUser represents an organization-user relation.
|
||||
type OrgUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
OrgID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
IsPublic bool `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OrgUser))
|
||||
}
|
||||
|
||||
// ErrUserHasOrgs represents a "UserHasOrgs" kind of error.
|
||||
type ErrUserHasOrgs struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserHasOrgs checks if an error is a ErrUserHasOrgs.
|
||||
func IsErrUserHasOrgs(err error) bool {
|
||||
_, ok := err.(ErrUserHasOrgs)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserHasOrgs) Error() string {
|
||||
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// GetOrganizationCount returns count of membership of organization of the user.
|
||||
func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("uid=?", u.ID).
|
||||
Count(new(OrgUser))
|
||||
}
|
||||
|
||||
// IsOrganizationOwner returns true if given user is in the owner team.
|
||||
func IsOrganizationOwner(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
ownerTeam, err := GetOwnerTeam(ctx, orgID)
|
||||
if err != nil {
|
||||
if IsErrTeamNotExist(err) {
|
||||
log.Error("Organization does not have owner team: %d", orgID)
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return IsTeamMember(ctx, orgID, ownerTeam.ID, uid)
|
||||
}
|
||||
|
||||
// IsOrganizationAdmin returns true if given user is in the owner team or an admin team.
|
||||
func IsOrganizationAdmin(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
teams, err := GetUserOrgTeams(ctx, orgID, uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, t := range teams {
|
||||
if t.HasAdminAccess() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsOrganizationMember returns true if given user is member of organization.
|
||||
func IsOrganizationMember(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("uid=?", uid).
|
||||
And("org_id=?", orgID).
|
||||
Table("org_user").
|
||||
Exist()
|
||||
}
|
||||
|
||||
// IsPublicMembership returns true if the given user's membership of given org is public.
|
||||
func IsPublicMembership(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("uid=?", uid).
|
||||
And("org_id=?", orgID).
|
||||
And("is_public=?", true).
|
||||
Table("org_user").
|
||||
Exist()
|
||||
}
|
||||
|
||||
// CanCreateOrgRepo returns true if user can create repo in organization
|
||||
func CanCreateOrgRepo(ctx context.Context, orgID, uid int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where(builder.Eq{"team.can_create_org_repo": true}).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
And("team_user.uid = ?", uid).
|
||||
And("team_user.org_id = ?", orgID).
|
||||
Exist(new(Team))
|
||||
}
|
||||
|
||||
// IsUserOrgOwner returns true if user is in the owner team of given organization.
|
||||
func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64) map[int64]bool {
|
||||
results := make(map[int64]bool, len(users))
|
||||
for _, user := range users {
|
||||
results[user.ID] = false // Set default to false
|
||||
}
|
||||
ownerMaps, err := loadOrganizationOwners(ctx, users, orgID)
|
||||
if err == nil {
|
||||
for _, owner := range ownerMaps {
|
||||
results[owner.UID] = true
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// GetOrgAssignees returns all users that have write access and can be assigned to issues
|
||||
// of the any repository in the organization.
|
||||
func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
userIDs := make([]int64, 0, 10)
|
||||
if err = e.Table("access").
|
||||
Join("INNER", "repository", "`repository`.id = `access`.repo_id").
|
||||
Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite).
|
||||
Select("user_id").
|
||||
Find(&userIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
additionalUserIDs := make([]int64, 0, 10)
|
||||
if err = e.Table("team_user").
|
||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id").
|
||||
Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
||||
orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
||||
Distinct("`team_user`.uid").
|
||||
Select("`team_user`.uid").
|
||||
Find(&additionalUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uniqueUserIDs := make(container.Set[int64])
|
||||
uniqueUserIDs.AddMultiple(userIDs...)
|
||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||
|
||||
users := make([]*user_model.User, 0, len(uniqueUserIDs))
|
||||
if len(userIDs) > 0 {
|
||||
if err = e.In("id", uniqueUserIDs.Values()).
|
||||
Where(builder.Eq{"`user`.is_active": true}).
|
||||
OrderBy(user_model.GetOrderByName()).
|
||||
Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ownerTeam, err := GetOwnerTeam(ctx, orgID)
|
||||
if err != nil {
|
||||
if IsErrTeamNotExist(err) {
|
||||
log.Error("Organization does not have owner team: %d", orgID)
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := users.GetUserIDs()
|
||||
ownerMaps := make(map[int64]*TeamUser)
|
||||
err = db.GetEngine(ctx).In("uid", userIDs).
|
||||
And("org_id=?", orgID).
|
||||
And("team_id=?", ownerTeam.ID).
|
||||
Find(&ownerMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find team users: %w", err)
|
||||
}
|
||||
return ownerMaps, nil
|
||||
}
|
154
models/organization/org_user_test.go
Normal file
154
models/organization/org_user_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserIsPublicMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
tt := []struct {
|
||||
uid int64
|
||||
orgid int64
|
||||
expected bool
|
||||
}{
|
||||
{2, 3, true},
|
||||
{4, 3, false},
|
||||
{5, 6, true},
|
||||
{5, 7, false},
|
||||
}
|
||||
for _, v := range tt {
|
||||
t.Run(fmt.Sprintf("UserId%dIsPublicMemberOf%d", v.uid, v.orgid), func(t *testing.T) {
|
||||
testUserIsPublicMember(t, v.uid, v.orgid, v.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testUserIsPublicMember(t *testing.T, uid, orgID int64, expected bool) {
|
||||
user, err := user_model.GetUserByID(db.DefaultContext, uid)
|
||||
assert.NoError(t, err)
|
||||
is, err := organization.IsPublicMembership(db.DefaultContext, orgID, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, is)
|
||||
}
|
||||
|
||||
func TestIsUserOrgOwner(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
tt := []struct {
|
||||
uid int64
|
||||
orgid int64
|
||||
expected bool
|
||||
}{
|
||||
{2, 3, true},
|
||||
{4, 3, false},
|
||||
{5, 6, true},
|
||||
{5, 7, true},
|
||||
}
|
||||
for _, v := range tt {
|
||||
t.Run(fmt.Sprintf("UserId%dIsOrgOwnerOf%d", v.uid, v.orgid), func(t *testing.T) {
|
||||
testIsUserOrgOwner(t, v.uid, v.orgid, v.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testIsUserOrgOwner(t *testing.T, uid, orgID int64, expected bool) {
|
||||
user, err := user_model.GetUserByID(db.DefaultContext, uid)
|
||||
assert.NoError(t, err)
|
||||
is, err := organization.IsOrganizationOwner(db.DefaultContext, orgID, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, is)
|
||||
}
|
||||
|
||||
func TestUserListIsPublicMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
tt := []struct {
|
||||
orgid int64
|
||||
expected map[int64]bool
|
||||
}{
|
||||
{3, map[int64]bool{2: true, 4: false, 28: true}},
|
||||
{6, map[int64]bool{5: true, 28: true}},
|
||||
{7, map[int64]bool{5: false}},
|
||||
{25, map[int64]bool{12: true, 24: true}},
|
||||
{22, map[int64]bool{}},
|
||||
}
|
||||
for _, v := range tt {
|
||||
t.Run(fmt.Sprintf("IsPublicMemberOfOrgId%d", v.orgid), func(t *testing.T) {
|
||||
testUserListIsPublicMember(t, v.orgid, v.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) {
|
||||
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
||||
assert.NoError(t, err)
|
||||
_, membersIsPublic, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, membersIsPublic)
|
||||
}
|
||||
|
||||
func TestUserListIsUserOrgOwner(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
tt := []struct {
|
||||
orgid int64
|
||||
expected map[int64]bool
|
||||
}{
|
||||
{3, map[int64]bool{2: true, 4: false, 28: false}},
|
||||
{6, map[int64]bool{5: true, 28: false}},
|
||||
{7, map[int64]bool{5: true}},
|
||||
{25, map[int64]bool{12: true, 24: false}}, // ErrTeamNotExist
|
||||
{22, map[int64]bool{}}, // No member
|
||||
}
|
||||
for _, v := range tt {
|
||||
t.Run(fmt.Sprintf("IsUserOrgOwnerOfOrgId%d", v.orgid), func(t *testing.T) {
|
||||
testUserListIsUserOrgOwner(t, v.orgid, v.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) {
|
||||
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
|
||||
assert.NoError(t, err)
|
||||
members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID))
|
||||
}
|
||||
|
||||
func TestAddOrgUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(orgID, userID int64, isPublic bool) {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||
expectedNumMembers := org.NumMembers
|
||||
if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil {
|
||||
expectedNumMembers++
|
||||
}
|
||||
assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID))
|
||||
ou := &organization.OrgUser{OrgID: orgID, UID: userID}
|
||||
unittest.AssertExistsAndLoadBean(t, ou)
|
||||
assert.Equal(t, isPublic, ou.IsPublic)
|
||||
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||
assert.Equal(t, expectedNumMembers, org.NumMembers)
|
||||
}
|
||||
|
||||
setting.Service.DefaultOrgMemberVisible = false
|
||||
testSuccess(3, 5, false)
|
||||
testSuccess(3, 5, false)
|
||||
testSuccess(6, 2, false)
|
||||
|
||||
setting.Service.DefaultOrgMemberVisible = true
|
||||
testSuccess(6, 3, true)
|
||||
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
103
models/organization/org_worktime.go
Normal file
103
models/organization/org_worktime.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type WorktimeSumByRepos struct {
|
||||
RepoName string
|
||||
SumTime int64
|
||||
}
|
||||
|
||||
func GetWorktimeByRepos(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("repository.name AS repo_name, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("repository.name").
|
||||
OrderBy("repository.name").
|
||||
Find(&results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
type WorktimeSumByMilestones struct {
|
||||
RepoName string
|
||||
MilestoneName string
|
||||
MilestoneID int64
|
||||
MilestoneDeadline int64
|
||||
SumTime int64
|
||||
HideRepoName bool
|
||||
}
|
||||
|
||||
func GetWorktimeByMilestones(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("repository.name AS repo_name, milestone.name AS milestone_name, milestone.id AS milestone_id, milestone.deadline_unix as milestone_deadline, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Join("LEFT", "milestone", "issue.milestone_id = milestone.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("repository.name, milestone.name, milestone.deadline_unix, milestone.id").
|
||||
OrderBy("repository.name, milestone.deadline_unix, milestone.id").
|
||||
Find(&results)
|
||||
|
||||
// TODO: pgsql: NULL values are sorted last in default ascending order, so we need to sort them manually again.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].RepoName != results[j].RepoName {
|
||||
return results[i].RepoName < results[j].RepoName
|
||||
}
|
||||
if results[i].MilestoneDeadline != results[j].MilestoneDeadline {
|
||||
return results[i].MilestoneDeadline < results[j].MilestoneDeadline
|
||||
}
|
||||
return results[i].MilestoneID < results[j].MilestoneID
|
||||
})
|
||||
|
||||
// Show only the first RepoName, for nicer output.
|
||||
prevRepoName := ""
|
||||
for i := 0; i < len(results); i++ {
|
||||
res := &results[i]
|
||||
res.MilestoneDeadline = 0 // clear the deadline because we do not really need it
|
||||
if prevRepoName == res.RepoName {
|
||||
res.HideRepoName = true
|
||||
}
|
||||
prevRepoName = res.RepoName
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
||||
type WorktimeSumByMembers struct {
|
||||
UserName string
|
||||
SumTime int64
|
||||
}
|
||||
|
||||
func GetWorktimeByMembers(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("`user`.name AS user_name, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Join("INNER", "`user`", "tracked_time.user_id = `user`.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("`user`.name").
|
||||
OrderBy("sum_time DESC").
|
||||
Find(&results)
|
||||
return results, err
|
||||
}
|
249
models/organization/team.go
Normal file
249
models/organization/team.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___________
|
||||
// \__ ___/___ _____ _____
|
||||
// | |_/ __ \\__ \ / \
|
||||
// | |\ ___/ / __ \| Y Y \
|
||||
// |____| \___ >____ /__|_| /
|
||||
// \/ \/ \/
|
||||
|
||||
// ErrTeamAlreadyExist represents a "TeamAlreadyExist" kind of error.
|
||||
type ErrTeamAlreadyExist struct {
|
||||
OrgID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrTeamAlreadyExist checks if an error is a ErrTeamAlreadyExist.
|
||||
func IsErrTeamAlreadyExist(err error) bool {
|
||||
_, ok := err.(ErrTeamAlreadyExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
|
||||
}
|
||||
|
||||
func (err ErrTeamAlreadyExist) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// ErrTeamNotExist represents a "TeamNotExist" error
|
||||
type ErrTeamNotExist struct {
|
||||
OrgID int64
|
||||
TeamID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrTeamNotExist checks if an error is a ErrTeamNotExist.
|
||||
func IsErrTeamNotExist(err error) bool {
|
||||
_, ok := err.(ErrTeamNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamNotExist) Error() string {
|
||||
return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name)
|
||||
}
|
||||
|
||||
func (err ErrTeamNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// OwnerTeamName return the owner team name
|
||||
const OwnerTeamName = "Owners"
|
||||
|
||||
// Team represents a organization team.
|
||||
type Team struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
LowerName string
|
||||
Name string
|
||||
Description string
|
||||
AccessMode perm.AccessMode `xorm:"'authorize'"`
|
||||
Members []*user_model.User `xorm:"-"`
|
||||
NumRepos int
|
||||
NumMembers int
|
||||
Units []*TeamUnit `xorm:"-"`
|
||||
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Team))
|
||||
db.RegisterModel(new(TeamUser))
|
||||
db.RegisterModel(new(TeamRepo))
|
||||
db.RegisterModel(new(TeamUnit))
|
||||
db.RegisterModel(new(TeamInvite))
|
||||
}
|
||||
|
||||
func (t *Team) LogString() string {
|
||||
if t == nil {
|
||||
return "<Team nil>"
|
||||
}
|
||||
return fmt.Sprintf("<Team %d:%s OrgID=%d AccessMode=%s>", t.ID, t.Name, t.OrgID, t.AccessMode.LogString())
|
||||
}
|
||||
|
||||
// LoadUnits load a list of available units for a team
|
||||
func (t *Team) LoadUnits(ctx context.Context) (err error) {
|
||||
if t.Units != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Units, err = getUnitsByTeamID(ctx, t.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUnitNames returns the team units names
|
||||
func (t *Team) GetUnitNames() (res []string) {
|
||||
if t.HasAdminAccess() {
|
||||
return unit.AllUnitKeyNames()
|
||||
}
|
||||
|
||||
for _, u := range t.Units {
|
||||
res = append(res, unit.Units[u.Type].NameKey)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// GetUnitsMap returns the team units permissions
|
||||
func (t *Team) GetUnitsMap() map[string]string {
|
||||
m := make(map[string]string)
|
||||
if t.HasAdminAccess() {
|
||||
for _, u := range unit.Units {
|
||||
m[u.NameKey] = t.AccessMode.ToString()
|
||||
}
|
||||
} else {
|
||||
for _, u := range t.Units {
|
||||
m[u.Unit().NameKey] = u.AccessMode.ToString()
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// IsOwnerTeam returns true if team is owner team.
|
||||
func (t *Team) IsOwnerTeam() bool {
|
||||
return t.Name == OwnerTeamName
|
||||
}
|
||||
|
||||
// IsMember returns true if given user is a member of team.
|
||||
func (t *Team) IsMember(ctx context.Context, userID int64) bool {
|
||||
isMember, err := IsTeamMember(ctx, t.OrgID, t.ID, userID)
|
||||
if err != nil {
|
||||
log.Error("IsMember: %v", err)
|
||||
return false
|
||||
}
|
||||
return isMember
|
||||
}
|
||||
|
||||
func (t *Team) HasAdminAccess() bool {
|
||||
return t.AccessMode >= perm.AccessModeAdmin
|
||||
}
|
||||
|
||||
// LoadMembers returns paginated members in team of organization.
|
||||
func (t *Team) LoadMembers(ctx context.Context) (err error) {
|
||||
t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{
|
||||
TeamID: t.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// UnitEnabled returns true if the team has the given unit type enabled
|
||||
func (t *Team) UnitEnabled(ctx context.Context, tp unit.Type) bool {
|
||||
return t.UnitAccessMode(ctx, tp) > perm.AccessModeNone
|
||||
}
|
||||
|
||||
// UnitAccessMode returns the access mode for the given unit type, "none" for non-existent units
|
||||
func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode {
|
||||
accessMode, _ := t.UnitAccessModeEx(ctx, tp)
|
||||
return accessMode
|
||||
}
|
||||
|
||||
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) {
|
||||
if err := t.LoadUnits(ctx); err != nil {
|
||||
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
|
||||
}
|
||||
for _, u := range t.Units {
|
||||
if u.Type == tp {
|
||||
return u.AccessMode, true
|
||||
}
|
||||
}
|
||||
return perm.AccessModeNone, false
|
||||
}
|
||||
|
||||
// IsUsableTeamName tests if a name could be as team name
|
||||
func IsUsableTeamName(name string) error {
|
||||
switch name {
|
||||
case "new":
|
||||
return db.ErrNameReserved{Name: name}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetTeam returns team by given team name and organization.
|
||||
func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) {
|
||||
t, exist, err := db.Get[Team](ctx, builder.Eq{"org_id": orgID, "lower_name": strings.ToLower(name)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, ErrTeamNotExist{orgID, 0, name}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// GetTeamIDsByNames returns a slice of team ids corresponds to names.
|
||||
func GetTeamIDsByNames(ctx context.Context, orgID int64, names []string, ignoreNonExistent bool) ([]int64, error) {
|
||||
ids := make([]int64, 0, len(names))
|
||||
for _, name := range names {
|
||||
u, err := GetTeam(ctx, orgID, name)
|
||||
if err != nil {
|
||||
if ignoreNonExistent {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, u.ID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// GetOwnerTeam returns team by given team name and organization.
|
||||
func GetOwnerTeam(ctx context.Context, orgID int64) (*Team, error) {
|
||||
return GetTeam(ctx, orgID, OwnerTeamName)
|
||||
}
|
||||
|
||||
// GetTeamByID returns team by given ID.
|
||||
func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) {
|
||||
t := new(Team)
|
||||
has, err := db.GetEngine(ctx).ID(teamID).Get(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrTeamNotExist{0, teamID, ""}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
||||
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
||||
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
||||
return err
|
||||
}
|
161
models/organization/team_invite.go
Normal file
161
models/organization/team_invite.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type ErrTeamInviteAlreadyExist struct {
|
||||
TeamID int64
|
||||
Email string
|
||||
}
|
||||
|
||||
func IsErrTeamInviteAlreadyExist(err error) bool {
|
||||
_, ok := err.(ErrTeamInviteAlreadyExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamInviteAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("team invite already exists [team_id: %d, email: %s]", err.TeamID, err.Email)
|
||||
}
|
||||
|
||||
func (err ErrTeamInviteAlreadyExist) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
type ErrTeamInviteNotFound struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func IsErrTeamInviteNotFound(err error) bool {
|
||||
_, ok := err.(ErrTeamInviteNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamInviteNotFound) Error() string {
|
||||
return fmt.Sprintf("team invite was not found [token: %s]", err.Token)
|
||||
}
|
||||
|
||||
func (err ErrTeamInviteNotFound) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrUserEmailAlreadyAdded represents a "user by email already added to team" error.
|
||||
type ErrUserEmailAlreadyAdded struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsErrUserEmailAlreadyAdded checks if an error is a ErrUserEmailAlreadyAdded.
|
||||
func IsErrUserEmailAlreadyAdded(err error) bool {
|
||||
_, ok := err.(ErrUserEmailAlreadyAdded)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserEmailAlreadyAdded) Error() string {
|
||||
return fmt.Sprintf("user with email already added [email: %s]", err.Email)
|
||||
}
|
||||
|
||||
func (err ErrUserEmailAlreadyAdded) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// TeamInvite represents an invite to a team
|
||||
type TeamInvite struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Token string `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"`
|
||||
InviterID int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
|
||||
TeamID int64 `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"`
|
||||
Email string `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, email string) (*TeamInvite, error) {
|
||||
has, err := db.GetEngine(ctx).Exist(&TeamInvite{
|
||||
TeamID: team.ID,
|
||||
Email: email,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return nil, ErrTeamInviteAlreadyExist{
|
||||
TeamID: team.ID,
|
||||
Email: email,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the user is already a team member by email
|
||||
exist, err := db.GetEngine(ctx).
|
||||
Where(builder.Eq{
|
||||
"team_user.org_id": team.OrgID,
|
||||
"team_user.team_id": team.ID,
|
||||
"`user`.email": email,
|
||||
}).
|
||||
Join("INNER", "`user`", "`user`.id = team_user.uid").
|
||||
Table("team_user").
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exist {
|
||||
return nil, ErrUserEmailAlreadyAdded{
|
||||
Email: email,
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.CryptoRandomString(25)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invite := &TeamInvite{
|
||||
Token: token,
|
||||
InviterID: doer.ID,
|
||||
OrgID: team.OrgID,
|
||||
TeamID: team.ID,
|
||||
Email: email,
|
||||
}
|
||||
|
||||
return invite, db.Insert(ctx, invite)
|
||||
}
|
||||
|
||||
func RemoveInviteByID(ctx context.Context, inviteID, teamID int64) error {
|
||||
_, err := db.DeleteByBean(ctx, &TeamInvite{
|
||||
ID: inviteID,
|
||||
TeamID: teamID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func GetInvitesByTeamID(ctx context.Context, teamID int64) ([]*TeamInvite, error) {
|
||||
invites := make([]*TeamInvite, 0, 10)
|
||||
return invites, db.GetEngine(ctx).
|
||||
Where("team_id=?", teamID).
|
||||
Find(&invites)
|
||||
}
|
||||
|
||||
func GetInviteByToken(ctx context.Context, token string) (*TeamInvite, error) {
|
||||
invite := &TeamInvite{}
|
||||
|
||||
has, err := db.GetEngine(ctx).Where("token=?", token).Get(invite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrTeamInviteNotFound{Token: token}
|
||||
}
|
||||
return invite, nil
|
||||
}
|
48
models/organization/team_invite_test.go
Normal file
48
models/organization/team_invite_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeamInvite(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
|
||||
t.Run("MailExistsInTeam", func(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// user 2 already added to team 2, should result in error
|
||||
_, err := organization.CreateTeamInvite(db.DefaultContext, user2, team, user2.Email)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("CreateAndRemove", func(t *testing.T) {
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
invite, err := organization.CreateTeamInvite(db.DefaultContext, user1, team, "org3@example.com")
|
||||
assert.NotNil(t, invite)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Shouldn't allow duplicate invite
|
||||
_, err = organization.CreateTeamInvite(db.DefaultContext, user1, team, "org3@example.com")
|
||||
assert.Error(t, err)
|
||||
|
||||
// should remove invite
|
||||
assert.NoError(t, organization.RemoveInviteByID(db.DefaultContext, invite.ID, invite.TeamID))
|
||||
|
||||
// invite should not exist
|
||||
_, err = organization.GetInviteByToken(db.DefaultContext, invite.Token)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
140
models/organization/team_list.go
Normal file
140
models/organization/team_list.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type TeamList []*Team
|
||||
|
||||
func (t TeamList) LoadUnits(ctx context.Context) error {
|
||||
for _, team := range t {
|
||||
if err := team.LoadUnits(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TeamList) UnitMaxAccess(tp unit.Type) perm.AccessMode {
|
||||
maxAccess := perm.AccessModeNone
|
||||
for _, team := range t {
|
||||
if team.IsOwnerTeam() {
|
||||
return perm.AccessModeOwner
|
||||
}
|
||||
for _, teamUnit := range team.Units {
|
||||
if teamUnit.Type != tp {
|
||||
continue
|
||||
}
|
||||
if teamUnit.AccessMode > maxAccess {
|
||||
maxAccess = teamUnit.AccessMode
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxAccess
|
||||
}
|
||||
|
||||
// SearchTeamOptions holds the search options
|
||||
type SearchTeamOptions struct {
|
||||
db.ListOptions
|
||||
UserID int64
|
||||
Keyword string
|
||||
OrgID int64
|
||||
IncludeDesc bool
|
||||
}
|
||||
|
||||
func (opts *SearchTeamOptions) toCond() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.Keyword) > 0 {
|
||||
lowerKeyword := strings.ToLower(opts.Keyword)
|
||||
var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
|
||||
if opts.IncludeDesc {
|
||||
keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
|
||||
}
|
||||
cond = cond.And(keywordCond)
|
||||
}
|
||||
|
||||
if opts.OrgID > 0 {
|
||||
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
|
||||
}
|
||||
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
// SearchTeam search for teams. Caller is responsible to check permissions.
|
||||
func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
opts.SetDefaultValues()
|
||||
cond := opts.toCond()
|
||||
|
||||
if opts.UserID > 0 {
|
||||
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
|
||||
}
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
|
||||
teams := make([]*Team, 0, opts.PageSize)
|
||||
count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return teams, count, nil
|
||||
}
|
||||
|
||||
// GetRepoTeams gets the list of teams that has access to the repository
|
||||
func GetRepoTeams(ctx context.Context, orgID, repoID int64) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_repo.repo_id=?", repoID).
|
||||
OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserOrgTeams returns all teams that user belongs to in given organization.
|
||||
func GetUserOrgTeams(ctx context.Context, orgID, userID int64) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserRepoTeams returns user repo's teams
|
||||
func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
And("team_repo.repo_id=?", repoID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) {
|
||||
teams := make([]*Team, 0, 10)
|
||||
return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams)
|
||||
}
|
||||
|
||||
func GetTeamsByIDs(ctx context.Context, teamIDs []int64) (map[int64]*Team, error) {
|
||||
teams := make(map[int64]*Team, len(teamIDs))
|
||||
if len(teamIDs) == 0 {
|
||||
return teams, nil
|
||||
}
|
||||
return teams, db.GetEngine(ctx).Where(builder.In("`id`", teamIDs)).Find(&teams)
|
||||
}
|
25
models/organization/team_list_test.go
Normal file
25
models/organization/team_list_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_GetTeamsByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// 1 owner team, 2 normal team
|
||||
teams, err := org_model.GetTeamsByIDs(db.DefaultContext, []int64{1, 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teams, 2)
|
||||
assert.Equal(t, "Owners", teams[1].Name)
|
||||
assert.Equal(t, "team1", teams[2].Name)
|
||||
}
|
76
models/organization/team_repo.go
Normal file
76
models/organization/team_repo.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// TeamRepo represents an team-repository relation.
|
||||
type TeamRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
TeamID int64 `xorm:"UNIQUE(s)"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
}
|
||||
|
||||
// HasTeamRepo returns true if given repository belongs to team.
|
||||
func HasTeamRepo(ctx context.Context, orgID, teamID, repoID int64) bool {
|
||||
has, _ := db.GetEngine(ctx).
|
||||
Where("org_id=?", orgID).
|
||||
And("team_id=?", teamID).
|
||||
And("repo_id=?", repoID).
|
||||
Get(new(TeamRepo))
|
||||
return has
|
||||
}
|
||||
|
||||
// AddTeamRepo adds a repo for an organization's team
|
||||
func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Insert(&TeamRepo{
|
||||
OrgID: orgID,
|
||||
TeamID: teamID,
|
||||
RepoID: repoID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveTeamRepo remove repository from team
|
||||
func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
||||
_, err := db.DeleteByBean(ctx, &TeamRepo{
|
||||
TeamID: teamID,
|
||||
RepoID: repoID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
|
||||
sub := builder.Select("team_id").From("team_unit").
|
||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||
|
||||
err := db.GetEngine(ctx).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And(builder.Or(
|
||||
builder.Expr("team.authorize >= ?", mode),
|
||||
builder.In("team.id", sub),
|
||||
)).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
|
||||
return teams, err
|
||||
}
|
31
models/organization/team_repo_test.go
Normal file
31
models/organization/team_repo_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
||||
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 2) {
|
||||
assert.EqualValues(t, 21, teams[0].ID)
|
||||
assert.EqualValues(t, 22, teams[1].ID)
|
||||
}
|
||||
}
|
208
models/organization/team_test.go
Normal file
208
models/organization/team_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeam_IsOwnerTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
assert.True(t, team.IsOwnerTeam())
|
||||
|
||||
team = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
assert.False(t, team.IsOwnerTeam())
|
||||
}
|
||||
|
||||
func TestTeam_IsMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
assert.True(t, team.IsMember(db.DefaultContext, 2))
|
||||
assert.False(t, team.IsMember(db.DefaultContext, 4))
|
||||
assert.False(t, team.IsMember(db.DefaultContext, unittest.NonexistentID))
|
||||
|
||||
team = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
assert.True(t, team.IsMember(db.DefaultContext, 2))
|
||||
assert.True(t, team.IsMember(db.DefaultContext, 4))
|
||||
assert.False(t, team.IsMember(db.DefaultContext, unittest.NonexistentID))
|
||||
}
|
||||
|
||||
func TestTeam_GetRepositories(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{
|
||||
TeamID: team.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, team.NumRepos)
|
||||
for _, repo := range repos {
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID})
|
||||
}
|
||||
}
|
||||
test(1)
|
||||
test(3)
|
||||
}
|
||||
|
||||
func TestTeam_GetMembers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, team.LoadMembers(db.DefaultContext))
|
||||
assert.Len(t, team.Members, team.NumMembers)
|
||||
for _, member := range team.Members {
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: member.ID, TeamID: teamID})
|
||||
}
|
||||
}
|
||||
test(1)
|
||||
test(3)
|
||||
}
|
||||
|
||||
func TestGetTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(orgID int64, name string) {
|
||||
team, err := organization.GetTeam(db.DefaultContext, orgID, name)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, orgID, team.OrgID)
|
||||
assert.Equal(t, name, team.Name)
|
||||
}
|
||||
testSuccess(3, "Owners")
|
||||
testSuccess(3, "team1")
|
||||
|
||||
_, err := organization.GetTeam(db.DefaultContext, 3, "nonexistent")
|
||||
assert.Error(t, err)
|
||||
_, err = organization.GetTeam(db.DefaultContext, unittest.NonexistentID, "Owners")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetTeamByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(teamID int64) {
|
||||
team, err := organization.GetTeamByID(db.DefaultContext, teamID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, teamID, team.ID)
|
||||
}
|
||||
testSuccess(1)
|
||||
testSuccess(2)
|
||||
testSuccess(3)
|
||||
testSuccess(4)
|
||||
|
||||
_, err := organization.GetTeamByID(db.DefaultContext, unittest.NonexistentID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestIsTeamMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(orgID, teamID, userID int64, expected bool) {
|
||||
isMember, err := organization.IsTeamMember(db.DefaultContext, orgID, teamID, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, isMember)
|
||||
}
|
||||
|
||||
test(3, 1, 2, true)
|
||||
test(3, 1, 4, false)
|
||||
test(3, 1, unittest.NonexistentID, false)
|
||||
|
||||
test(3, 2, 2, true)
|
||||
test(3, 2, 4, true)
|
||||
|
||||
test(3, unittest.NonexistentID, unittest.NonexistentID, false)
|
||||
test(unittest.NonexistentID, unittest.NonexistentID, unittest.NonexistentID, false)
|
||||
}
|
||||
|
||||
func TestGetTeamMembers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
members, err := organization.GetTeamMembers(db.DefaultContext, &organization.SearchMembersOptions{
|
||||
TeamID: teamID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, members, team.NumMembers)
|
||||
for _, member := range members {
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: member.ID, TeamID: teamID})
|
||||
}
|
||||
}
|
||||
test(1)
|
||||
test(3)
|
||||
}
|
||||
|
||||
func TestGetUserTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(userID int64) {
|
||||
teams, _, err := organization.SearchTeam(db.DefaultContext, &organization.SearchTeamOptions{UserID: userID})
|
||||
assert.NoError(t, err)
|
||||
for _, team := range teams {
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{TeamID: team.ID, UID: userID})
|
||||
}
|
||||
}
|
||||
test(2)
|
||||
test(5)
|
||||
test(unittest.NonexistentID)
|
||||
}
|
||||
|
||||
func TestGetUserOrgTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(orgID, userID int64) {
|
||||
teams, err := organization.GetUserOrgTeams(db.DefaultContext, orgID, userID)
|
||||
assert.NoError(t, err)
|
||||
for _, team := range teams {
|
||||
assert.Equal(t, orgID, team.OrgID)
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{TeamID: team.ID, UID: userID})
|
||||
}
|
||||
}
|
||||
test(3, 2)
|
||||
test(3, 4)
|
||||
test(3, unittest.NonexistentID)
|
||||
}
|
||||
|
||||
func TestHasTeamRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID, repoID int64, expected bool) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.Equal(t, expected, organization.HasTeamRepo(db.DefaultContext, team.OrgID, teamID, repoID))
|
||||
}
|
||||
test(1, 1, false)
|
||||
test(1, 3, true)
|
||||
test(1, 5, true)
|
||||
test(1, unittest.NonexistentID, false)
|
||||
|
||||
test(2, 3, true)
|
||||
test(2, 5, false)
|
||||
}
|
||||
|
||||
func TestUsersInTeamsCount(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamIDs, userIDs []int64, expected int64) {
|
||||
count, err := organization.UsersInTeamsCount(db.DefaultContext, teamIDs, userIDs)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, count)
|
||||
}
|
||||
|
||||
test([]int64{2}, []int64{1, 2, 3, 4}, 1) // only userid 2
|
||||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2) // userid 2,4
|
||||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5
|
||||
}
|
||||
|
||||
func TestIsUsableTeamName(t *testing.T) {
|
||||
assert.NoError(t, organization.IsUsableTeamName("usable"))
|
||||
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))
|
||||
}
|
51
models/organization/team_unit.go
Normal file
51
models/organization/team_unit.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
)
|
||||
|
||||
// TeamUnit describes all units of a repository
|
||||
type TeamUnit struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
TeamID int64 `xorm:"UNIQUE(s)"`
|
||||
Type unit.Type `xorm:"UNIQUE(s)"`
|
||||
AccessMode perm.AccessMode
|
||||
}
|
||||
|
||||
// Unit returns Unit
|
||||
func (t *TeamUnit) Unit() unit.Unit {
|
||||
return unit.Units[t.Type]
|
||||
}
|
||||
|
||||
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
|
||||
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
|
||||
}
|
||||
|
||||
// UpdateTeamUnits updates a teams's units
|
||||
func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(units) > 0 {
|
||||
if err = db.Insert(ctx, units); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
81
models/organization/team_user.go
Normal file
81
models/organization/team_user.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// TeamUser represents an team-user relation.
|
||||
type TeamUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
TeamID int64 `xorm:"UNIQUE(s)"`
|
||||
UID int64 `xorm:"UNIQUE(s)"`
|
||||
}
|
||||
|
||||
// IsTeamMember returns true if given user is a member of team.
|
||||
func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("org_id=?", orgID).
|
||||
And("team_id=?", teamID).
|
||||
And("uid=?", userID).
|
||||
Table("team_user").
|
||||
Exist()
|
||||
}
|
||||
|
||||
// SearchMembersOptions holds the search options
|
||||
type SearchMembersOptions struct {
|
||||
db.ListOptions
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
func (opts SearchMembersOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.TeamID > 0 {
|
||||
cond = cond.And(builder.Eq{"": opts.TeamID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// GetTeamMembers returns all members in given team of organization.
|
||||
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
|
||||
var members []*user_model.User
|
||||
sess := db.GetEngine(ctx)
|
||||
if opts.TeamID > 0 {
|
||||
sess = sess.In("id",
|
||||
builder.Select("uid").
|
||||
From("team_user").
|
||||
Where(builder.Eq{"team_id": opts.TeamID}),
|
||||
)
|
||||
}
|
||||
if opts.PageSize > 0 && opts.Page > 0 {
|
||||
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
if err := sess.OrderBy("full_name, name").Find(&members); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// IsUserInTeams returns if a user in some teams
|
||||
func IsUserInTeams(ctx context.Context, userID int64, teamIDs []int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
|
||||
}
|
||||
|
||||
// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs
|
||||
func UsersInTeamsCount(ctx context.Context, userIDs, teamIDs []int64) (int64, error) {
|
||||
var ids []int64
|
||||
if err := db.GetEngine(ctx).In("uid", userIDs).In("team_id", teamIDs).
|
||||
Table("team_user").
|
||||
Cols("uid").GroupBy("uid").Find(&ids); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(len(ids)), nil
|
||||
}
|
Reference in New Issue
Block a user