snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:41:59 +05:00
commit 52ae0c5802
6280 changed files with 624193 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"gitea.dev/models/unittest"
_ "gitea.dev/models"
_ "gitea.dev/models/actions"
_ "gitea.dev/models/activities"
_ "gitea.dev/models/organization"
_ "gitea.dev/models/repo"
_ "gitea.dev/models/user"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}

619
models/organization/org.go Normal file
View File

@@ -0,0 +1,619 @@
// 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"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// 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)
}
// 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 {
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(ctx context.Context) string {
return org.AsUser().HTMLURL(ctx)
}
// 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 represents find org members conditions
type FindOrgMembersOpts struct {
db.ListOptions
Doer *user_model.User
IsDoerMember bool
OrgID int64
Keyword string
}
func (opts FindOrgMembersOpts) PublicOnly() bool {
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
}
// applyKeywordFilter adds keyword search conditions to session
func (opts FindOrgMembersOpts) applyKeywordFilter(sess db.Session) bool {
if opts.Keyword == "" {
return false
}
keywordCond := builder.Or(
db.BuildCaseInsensitiveLike("`user`.lower_name", opts.Keyword),
db.BuildCaseInsensitiveLike("`user`.full_name", opts.Keyword),
)
emailCond := db.BuildCaseInsensitiveLike("`user`.email", opts.Keyword)
switch {
case opts.Doer == nil:
emailCond = emailCond.And(builder.Eq{"`user`.keep_email_private": false})
case !opts.Doer.IsAdmin:
emailCond = emailCond.And(
builder.Or(
builder.Eq{"`user`.keep_email_private": false},
builder.Eq{"`user`.id": opts.Doer.ID},
),
)
}
keywordCond = keywordCond.Or(emailCond)
_ = sess.Join("INNER", "`user`", "org_user.uid = `user`.id").And(keywordCond)
return true
}
// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess db.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)
}
_ = opts.applyKeywordFilter(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
}
func (org *Organization) AnyRepoUnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
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.AnyRepoUnitMaxAccess(ctx, unitType)
}
}
if ownerVisibilitySatisfiesDoer(org.AsUser(), doer) {
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
return db.WithTx(ctx, func(ctx context.Context) error {
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,
Visibility: structs.VisibleTypeLimited,
}
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 nil
})
}
// 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, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization})
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)
}
func ownerVisibilitySatisfiesDoer(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 !setting.Service.RequireSignInViewStrict && orgOrUser.Visibility == structs.VisibleTypePublic {
return true
}
return orgOrUser.Visibility != structs.VisibleTypePrivate && !user.IsRestricted
}
// HasOrgOrUserVisible tells if the given user can see the given org or user
func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool {
return ownerVisibilitySatisfiesDoer(owner, doer) ||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))
}
// 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 {
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.applyKeywordFilter(sess) {
sess = sess.Select("org_user.*")
}
sess = sess.OrderBy("org_user.uid ASC")
if opts.ListOptions.PageSize > 0 {
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
}
return db.WithTx(ctx, func(ctx context.Context) error {
// 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 nil
})
}
// 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) (TeamList, error) {
teams := make(TeamList, 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,
})
}

View File

@@ -0,0 +1,177 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/perm"
user_model "gitea.dev/models/user"
"gitea.dev/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 (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.IncludeVisibility = structs.VisibleTypePublic
}
}
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
}
if doer.IsRestricted {
return structs.VisibleTypePublic
}
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) {
outputCols := []string{
"id",
"name",
"full_name",
"visibility",
"avatar",
"avatar_email",
"use_custom_avatar",
}
selectColumns := &strings.Builder{}
for i, col := range outputCols {
_, _ = fmt.Fprintf(selectColumns, "`user`.%s", 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
}

View File

@@ -0,0 +1,90 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/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(t.Context()).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
assert.NoError(t, err)
cnt, err := db.Count[organization.Organization](t.Context(), 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](t.Context(), 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](t.Context(), organization.FindOrgOptions{
UserID: 4,
})
assert.NoError(t, err)
assert.Empty(t, orgs)
total, err := db.Count[organization.Organization](t.Context(), 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(t.Context(), &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(t.Context(), &user_model.User{ID: 4})
assert.NoError(t, err)
assert.Len(t, orgs, 1)
teamsMap, err := organization.OrgList(orgs).LoadTeams(t.Context())
assert.NoError(t, err)
assert.Len(t, teamsMap, 1)
assert.Len(t, teamsMap[3], 5)
}
func testDoerViewOtherVisibility(t *testing.T) {
viewer := &user_model.User{ID: 1}
other := &user_model.User{ID: 2}
restrictedViewer := &user_model.User{ID: 3, IsRestricted: true}
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil))
assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(viewer, other))
assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(restrictedViewer, other))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(viewer, viewer))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(restrictedViewer, restrictedViewer))
assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 4, IsAdmin: true, IsRestricted: true}, other))
}

View File

@@ -0,0 +1,637 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"slices"
"sort"
"testing"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/test"
"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(t.Context(), 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(t.Context(), 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(t.Context(), "team1")
assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID)
assert.Equal(t, "team1", team.LowerName)
_, err = org.GetTeam(t.Context(), "does not exist")
assert.True(t, organization.IsErrTeamNotExist(err))
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetTeam(t.Context(), "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(t.Context())
assert.NoError(t, err)
assert.Equal(t, org.ID, team.OrgID)
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
_, err = nonOrg.GetOwnerTeam(t.Context())
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(t.Context())
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(t.Context(), &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(t.Context(), "org3")
assert.NoError(t, err)
assert.EqualValues(t, 3, org.ID)
assert.Equal(t, "org3", org.Name)
_, err = organization.GetOrgByName(t.Context(), "user2") // user2 is an individual
assert.True(t, organization.IsErrOrgNotExist(err))
_, err = organization.GetOrgByName(t.Context(), "") // 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(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), tc.opts)
assert.NoError(t, err)
assert.EqualValues(t, len(tc.expectedUIDs), count)
members, err := organization.GetOrgUsersByOrgID(t.Context(), 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(t.Context(), 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(t.Context(), opts)
assert.NoError(t, err)
assert.Len(t, orgUsers, 2)
orgUsers, err = organization.GetOrgUsersByOrgID(t.Context(), &organization.FindOrgMembersOpts{
ListOptions: db.ListOptions{},
OrgID: unittest.NonexistentID,
})
assert.NoError(t, err)
assert.Empty(t, orgUsers)
}
func TestOrgMembersSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
testCases := []struct {
name string
opts *organization.FindOrgMembersOpts
expectedUIDs []int64
}{
{
name: "match by username",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user4",
},
expectedUIDs: []int64{4},
},
{
name: "match by full name",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user27",
},
expectedUIDs: []int64{28},
},
{
name: "private email hidden",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user2@example.com",
},
expectedUIDs: []int64{},
},
{
name: "admin can search private email",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: admin,
Keyword: "user2@example.com",
},
expectedUIDs: []int64{2},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
count, err := organization.CountOrgMembers(t.Context(), tc.opts)
assert.NoError(t, err)
assert.EqualValues(t, len(tc.expectedUIDs), count)
members, err := organization.GetOrgUsersByOrgID(t.Context(), 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 TestChangeOrgUserStatus(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testSuccess := func(orgID, userID int64, public bool) {
assert.NoError(t, organization.ChangeOrgUserStatus(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), org, userID)
assert.NoError(t, err)
count, err := env.CountRepos(t.Context())
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(t.Context(), org, userID)
assert.NoError(t, err)
repoIDs, err := env.RepoIDs(t.Context())
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(t.Context(), org, userID)
assert.NoError(t, err)
repos, err := env.MirrorRepos(t.Context())
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(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), 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
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
require.True(t, restrictedUser.IsRestricted)
assert.True(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
assert.False(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
}
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(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), 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(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), 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(t.Context(), 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(t.Context(), 7)
assert.NoError(t, err)
assert.Len(t, users, 1)
assert.NotNil(t, users[5])
}
func TestCanCreateOrgRepoByOwnerTeamWithoutFlag(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
ownerTeam, err := org.GetOwnerTeam(t.Context())
require.NoError(t, err)
ownerTeam.CanCreateOrgRepo = false
_, err = db.GetEngine(t.Context()).ID(ownerTeam.ID).Cols("can_create_org_repo").Update(ownerTeam)
require.NoError(t, err)
ok, err := organization.CanCreateOrgRepo(t.Context(), org.ID, 2)
require.NoError(t, err)
assert.True(t, ok)
}
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(t.Context(), 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(t.Context(), org.ID, repo.ID))
unittest.AssertNotExistsBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID})
assert.NoError(t, organization.RemoveOrgRepo(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), 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(t.Context(), &organization.Organization{Name: "assets"}, owner)
assert.Error(t, err)
assert.True(t, db.IsErrNameReserved(err))
unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{})
}
func TestOrAnyRepoUnitPermission(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
org := organization.Organization{Visibility: structs.VisibleTypeLimited}
assert.Equal(t, perm.AccessModeNone, org.AnyRepoUnitPermission(t.Context(), nil, unit.TypeWiki))
assert.Equal(t, perm.AccessModeRead, org.AnyRepoUnitPermission(t.Context(), &user_model.User{}, unit.TypeWiki))
}

View File

@@ -0,0 +1,146 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"fmt"
"gitea.dev/models/db"
"gitea.dev/models/perm"
user_model "gitea.dev/models/user"
"gitea.dev/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))
}
func GetUserOrganizations(ctx context.Context, uid int64) (ret []*Organization, err error) {
err = db.GetEngine(ctx).Select("`user`.*").
Join("INNER", "org_user", "org_user.org_id = `user`.id").
Where("org_user.uid=?", uid).Find(&ret)
return ret, err
}
// 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)
}
// 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}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})).
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
}
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
if len(users) == 0 {
return nil, nil //nolint:nilnil // return nil when there are no users
}
ownerTeam, err := GetOwnerTeam(ctx, orgID)
if err != nil {
if IsErrTeamNotExist(err) {
log.Error("Organization does not have owner team: %d", orgID)
return nil, nil //nolint:nilnil // return nil when owner team does not exist
}
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
}

View File

@@ -0,0 +1,153 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"fmt"
"testing"
"gitea.dev/models/organization"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/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(t.Context(), uid)
assert.NoError(t, err)
is, err := organization.IsPublicMembership(t.Context(), 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(t.Context(), uid)
assert.NoError(t, err)
is, err := organization.IsOrganizationOwner(t.Context(), 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(t.Context(), orgID)
assert.NoError(t, err)
_, membersIsPublic, err := org.GetMembers(t.Context(), &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(t.Context(), orgID)
assert.NoError(t, err)
members, _, err := org.GetMembers(t.Context(), &user_model.User{IsAdmin: true})
assert.NoError(t, err)
assert.Equal(t, expected, organization.IsUserOrgOwner(t.Context(), 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(t.Context(), 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{})
}

View File

@@ -0,0 +1,104 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"sort"
"gitea.dev/models/db"
"xorm.io/builder"
)
type WorktimeSumByRepos struct {
RepoName string
SumTime int64
}
func GetWorktimeByRepos(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) {
err = db.GetEngine(ctx).
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(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) {
err = db.GetEngine(ctx).
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(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) {
err = db.GetEngine(ctx).
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
}

290
models/organization/team.go Normal file
View File

@@ -0,0 +1,290 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Copyright 2016 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"errors"
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/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:"-"`
// All repos in the org are included in this team automatically
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
// Any user with CanCreateOrgRepo permission can create a repository in the organization, regardless of team membership.
// And the user will become the repo's admin (via collaborator) after the creation.
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"`
}
func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() }
func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() }
func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() }
const (
ghostTeamID = -1
ghostTeamName = "(deleted team)"
)
func newGhostTeam() *Team {
return &Team{ID: ghostTeamID, Name: ghostTeamName, LowerName: ghostTeamName}
}
// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read
// the team's metadata, based on the team's visibility tier and the parent org's
// visibility. Privileged callers (site admins, org owners, team members) are
// decided by the caller before reaching here.
func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.User) (bool, error) {
switch t.Visibility {
case structs.VisibleTypePublic:
return HasOrgOrUserVisible(ctx, org, doer), nil
case structs.VisibleTypeLimited:
return IsOrganizationMember(ctx, t.OrgID, doer.ID)
default:
return false, nil
}
}
func NormalizeTeamVisibility[T ~string](v T) structs.VisibleType {
if vt, ok := structs.VisibilityModes[structs.VisibilityString(v)]; ok {
return vt
}
return structs.VisibleTypePrivate
}
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
}
// GetUnitsMap returns the team units permissions
func (t *Team) GetUnitsMap() map[string]string {
if len(t.Units) == 0 {
return nil
}
m := make(map[string]string)
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
}
// 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) (mode perm.AccessMode, exist bool) {
if err := t.LoadUnits(ctx); err != nil {
log.Error("Error loading team (ID: %d) units: %v", t.ID, err)
}
for _, u := range t.Units {
if u.Type == tp {
mode, exist = u.AccessMode, true
break
}
}
mode = max(mode, t.AccessMode)
if unitDef, ok := unit.Units[tp]; ok {
mode = min(mode, unitDef.MaxPerm())
}
return mode, exist || t.AccessMode > perm.AccessModeNone
}
// 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
}
func GetPossibleTeamByID(ctx context.Context, teamID int64) (int64, *Team, error) {
t, err := GetTeamByID(ctx, teamID)
if errors.Is(err, util.ErrNotExist) {
t = newGhostTeam()
return t.ID, t, nil
} else if err != nil {
return 0, nil, err
}
return t.ID, 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
}

View File

@@ -0,0 +1,158 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"fmt"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil"
"gitea.dev/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 := util.CryptoRandomString(25)
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
}

View File

@@ -0,0 +1,47 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"gitea.dev/models/organization"
"gitea.dev/models/unittest"
user_model "gitea.dev/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(t.Context(), 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(t.Context(), user1, team, "org3@example.com")
assert.NotNil(t, invite)
assert.NoError(t, err)
// Shouldn't allow duplicate invite
_, err = organization.CreateTeamInvite(t.Context(), user1, team, "org3@example.com")
assert.Error(t, err)
// should remove invite
assert.NoError(t, organization.RemoveInviteByID(t.Context(), invite.ID, invite.TeamID))
// invite should not exist
_, err = organization.GetInviteByToken(t.Context(), invite.Token)
assert.Error(t, err)
})
}

View File

@@ -0,0 +1,191 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
"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) AnyRepoUnitMaxAccess(ctx context.Context, tp unit.Type) perm.AccessMode {
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
maxAccess := perm.AccessModeNone
for _, team := range t {
if team.IsOwnerTeam() {
return perm.AccessModeOwner
}
maxAccess = max(maxAccess, team.UnitAccessMode(ctx, tp))
}
return maxAccess
}
func (t TeamList) HasAllRepoAdminAccess() bool {
for _, team := range t {
if team.IsOwnerTeam() || team.AccessMode == perm.AccessModeOwner {
return true
}
if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
return true
}
}
return false
}
// SearchTeamOptions holds the search options
type SearchTeamOptions struct {
db.ListOptions
UserID int64
Keyword string
OrgID int64
IncludeDesc bool
// IncludeVisibilities, when combined with UserID, also returns teams whose
// visibility is in this list, even if UserID is not a member. Typical values:
// - {limited,public} for org members
// - {public} for signed-in users who are not org members
// Leave empty to return only teams the user is a member of.
IncludeVisibilities []structs.VisibleType
}
func (opts *SearchTeamOptions) applyToSession(sess db.SQLSession) {
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})
}
switch {
case opts.UserID > 0 && len(opts.IncludeVisibilities) > 0:
sess = sess.Join("LEFT", "team_user", "team_user.team_id = team.id AND team_user.uid = ?", opts.UserID)
cond = cond.And(builder.Or(
builder.Eq{"team_user.uid": opts.UserID},
builder.In("`team`.visibility", opts.IncludeVisibilities),
))
case opts.UserID > 0:
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
case len(opts.IncludeVisibilities) > 0:
cond = cond.And(builder.In("`team`.visibility", opts.IncludeVisibilities))
}
sess.Where(cond)
}
func VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn bool) []structs.VisibleType {
switch {
case isOrgMember:
return []structs.VisibleType{structs.VisibleTypeLimited, structs.VisibleTypePublic}
case isSignedIn:
return []structs.VisibleType{structs.VisibleTypePublic}
default:
return nil
}
}
func ApplyTeamListFilter(ctx context.Context, orgID int64, viewer *user_model.User, isSignedIn bool, opts *SearchTeamOptions) error {
if viewer.IsAdmin {
return nil
}
isOwner, err := IsOrganizationOwner(ctx, orgID, viewer.ID)
if err != nil {
return err
}
if isOwner {
return nil
}
isOrgMember, err := IsOrganizationMember(ctx, orgID, viewer.ID)
if err != nil {
return err
}
opts.UserID = viewer.ID
opts.IncludeVisibilities = VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn)
return nil
}
// 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()
opts.applyToSession(sess)
db.SetSessionPagination(sess, opts)
teams := make([]*Team, 0, opts.PageSize)
count, err := sess.OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).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)
}

View File

@@ -0,0 +1,44 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
org_model "gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
"gitea.dev/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(t.Context(), []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)
}
func TestTeamList_UnitMaxAccess(t *testing.T) {
ctx := t.Context()
adminTeam := &org_model.Team{AccessMode: perm.AccessModeAdmin, Units: nil}
writeTeam := &org_model.Team{AccessMode: perm.AccessModeWrite, Units: nil}
granularTeam := &org_model.Team{
AccessMode: perm.AccessModeNone,
Units: []*org_model.TeamUnit{
{Type: unit.TypeCode, AccessMode: perm.AccessModeWrite},
},
}
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{writeTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeCode))
assert.Equal(t, perm.AccessModeNone, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{granularTeam, adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
}

View File

@@ -0,0 +1,96 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/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.
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
if err != nil {
return nil, err
}
if len(teamIDs) == 0 {
return teams, nil
}
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
return teams, err
}
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
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).
Select("team.id").
Table("team").
Join("INNER", "team_repo", "team_repo.team_id = team.id").
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
And(builder.Or(
builder.Expr("team.authorize >= ?", mode),
builder.In("team.id", sub),
)).
Find(&teamIDs)
return teamIDs, err
}
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
if err != nil {
return nil, err
}
if len(teamIDs) == 0 {
return userIDs, nil
}
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
return userIDs, err
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/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(t.Context(), 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)
}
}

View File

@@ -0,0 +1,320 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
"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(t.Context(), 2))
assert.False(t, team.IsMember(t.Context(), 4))
assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID))
team = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
assert.True(t, team.IsMember(t.Context(), 2))
assert.True(t, team.IsMember(t.Context(), 4))
assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID))
}
func TestTeam_CanNonMemberReadMeta(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // public org
org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 35}) // private org
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // member of org 3 and org 35
outsider := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // member of neither org
test := func(name string, team *organization.Team, org, doer *user_model.User, expected bool) {
t.Run(name, func(t *testing.T) {
ok, err := team.CanNonMemberReadMeta(t.Context(), org, doer)
assert.NoError(t, err)
assert.Equal(t, expected, ok)
})
}
// Public team is gated only by the parent org's visibility.
publicTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePublic}
test("public team, public org, member", publicTeam, org3, member, true)
test("public team, public org, outsider", publicTeam, org3, outsider, true)
// Public team inside a private org: only org members may see it.
publicTeamPrivOrg := &organization.Team{OrgID: 35, Visibility: structs.VisibleTypePublic}
test("public team, private org, org member", publicTeamPrivOrg, org35, member, true)
test("public team, private org, outsider", publicTeamPrivOrg, org35, outsider, false)
// Limited team: any org member, but never outsiders.
limitedTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypeLimited}
test("limited team, org member", limitedTeam, org3, member, true)
test("limited team, outsider", limitedTeam, org3, outsider, false)
// Private team is never visible to non-members; members/owners are admitted by the caller.
privateTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePrivate}
test("private team, org member", privateTeam, org3, member, false)
test("private team, outsider", privateTeam, org3, outsider, false)
}
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(t.Context(), &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(t.Context()))
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(t.Context(), 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(t.Context(), 3, "nonexistent")
assert.Error(t, err)
_, err = organization.GetTeam(t.Context(), 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(t.Context(), teamID)
assert.NoError(t, err)
assert.Equal(t, teamID, team.ID)
}
testSuccess(1)
testSuccess(2)
testSuccess(3)
testSuccess(4)
_, err := organization.GetTeamByID(t.Context(), 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(t.Context(), 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(t.Context(), &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(t.Context(), &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(t.Context(), 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 TestSearchTeamIncludeVisible(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const orgID int64 = 3
// User 5 is an org member but only belongs to team 1 (Owners) — make sure
// they don't see team 2 (default private) but do see a freshly added
// limited team they are not a member of.
visible := &organization.Team{
OrgID: orgID,
LowerName: "visible-team",
Name: "visible-team",
AccessMode: 1, // read
Visibility: structs.VisibleTypeLimited,
}
assert.NoError(t, db.Insert(t.Context(), visible))
teams, _, err := organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
OrgID: orgID,
UserID: 2,
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
})
assert.NoError(t, err)
ids := make(map[int64]bool, len(teams))
for _, team := range teams {
assert.Equal(t, orgID, team.OrgID)
ids[team.ID] = true
}
// user 2 is in team 1 and team 2 in org 3, plus should see the new visible team.
assert.True(t, ids[1], "expected to see team 1 (member)")
assert.True(t, ids[2], "expected to see team 2 (member)")
assert.True(t, ids[visible.ID], "expected to see visible team")
// user 5 is only an org member in team 1, must not see secret team 2 but must see the visible one.
teams, _, err = organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{
OrgID: orgID,
UserID: 5,
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true),
})
assert.NoError(t, err)
ids = make(map[int64]bool, len(teams))
for _, team := range teams {
ids[team.ID] = true
}
assert.False(t, ids[2], "user 5 must not see private team 2")
assert.True(t, ids[visible.ID], "user 5 must see the limited team")
}
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(t.Context(), 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(t.Context(), 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")))
}
func TestTeam_UnitAccessModeEx(t *testing.T) {
team := &organization.Team{
AccessMode: perm.AccessModeWrite, Units: []*organization.TeamUnit{
{Type: unit.TypeIssues, AccessMode: perm.AccessModeRead}, // team mode wins
{Type: unit.TypeWiki, AccessMode: perm.AccessModeAdmin}, // unit mode wins
},
}
mode, exist := team.UnitAccessModeEx(t.Context(), unit.TypeActions)
assert.True(t, exist)
assert.Equal(t, perm.AccessModeWrite, mode)
assert.Equal(t, perm.AccessModeWrite, team.UnitAccessMode(t.Context(), unit.TypeIssues))
assert.Equal(t, perm.AccessModeAdmin, team.UnitAccessMode(t.Context(), unit.TypeWiki))
assert.Equal(t, perm.AccessModeRead, team.UnitAccessMode(t.Context(), unit.TypeExternalWiki)) // limited by unit definition
team = &organization.Team{AccessMode: perm.AccessModeOwner, Units: []*organization.TeamUnit{}}
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypePackages)
assert.True(t, exist)
assert.Equal(t, perm.AccessModeAdmin, mode)
team = &organization.Team{AccessMode: perm.AccessModeNone, Units: []*organization.TeamUnit{}}
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypeActions)
assert.False(t, exist)
assert.Equal(t, perm.AccessModeNone, mode)
}

View File

@@ -0,0 +1,30 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/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)
}

View File

@@ -0,0 +1,73 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"gitea.dev/models/db"
user_model "gitea.dev/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
}
// 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
}