forgejo/models/organization/team.go
Gusted 9de9034400
[BUG] Don't allow owner team with incorrect unit access
- On editting a team, only update the units if the team isn't the
'Owners' team. Otherwise the 'Owners' team end up having all of their
unit access modes set to 'None'; because the request form doesn't send
over any units, as it's simply not shown in the UI.
- Adds a database inconstency check and fix for the case where the
'Owners' team is affected by this bug.
- Adds unit test.
- Adds integration test.
- Resolves #5528
- Regression of https://github.com/go-gitea/gitea/pull/24012
2024-10-11 14:48:47 +02:00

310 lines
8.5 KiB
Go

// Copyright 2018 The Gitea Authors. All rights reserved.
// Copyright 2016 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ___________
// \__ ___/___ _____ _____
// | |_/ __ \\__ \ / \
// | |\ ___/ / __ \| Y Y \
// |____| \___ >____ /__|_| /
// \/ \/ \/
// ErrTeamAlreadyExist represents a "TeamAlreadyExist" kind of error.
type ErrTeamAlreadyExist struct {
OrgID int64
Name string
}
// IsErrTeamAlreadyExist checks if an error is a ErrTeamAlreadyExist.
func IsErrTeamAlreadyExist(err error) bool {
_, ok := err.(ErrTeamAlreadyExist)
return ok
}
func (err ErrTeamAlreadyExist) Error() string {
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
}
func (err ErrTeamAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrTeamNotExist represents a "TeamNotExist" error
type ErrTeamNotExist struct {
OrgID int64
TeamID int64
Name string
}
// IsErrTeamNotExist checks if an error is a ErrTeamNotExist.
func IsErrTeamNotExist(err error) bool {
_, ok := err.(ErrTeamNotExist)
return ok
}
func (err ErrTeamNotExist) Error() string {
return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name)
}
func (err ErrTeamNotExist) Unwrap() error {
return util.ErrNotExist
}
// OwnerTeamName return the owner team name
const OwnerTeamName = "Owners"
// Team represents a organization team.
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
LowerName string
Name string
Description string
AccessMode perm.AccessMode `xorm:"'authorize'"`
Repos []*repo_model.Repository `xorm:"-"`
Members []*user_model.User `xorm:"-"`
NumRepos int
NumMembers int
Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
}
func init() {
db.RegisterModel(new(Team))
db.RegisterModel(new(TeamUser))
db.RegisterModel(new(TeamRepo))
db.RegisterModel(new(TeamUnit))
db.RegisterModel(new(TeamInvite))
}
func (t *Team) LogString() string {
if t == nil {
return "<Team nil>"
}
return fmt.Sprintf("<Team %d:%s OrgID=%d AccessMode=%s>", t.ID, t.Name, t.OrgID, t.AccessMode.LogString())
}
// LoadUnits load a list of available units for a team
func (t *Team) LoadUnits(ctx context.Context) (err error) {
if t.Units != nil {
return nil
}
t.Units, err = getUnitsByTeamID(ctx, t.ID)
return err
}
// GetUnitNames returns the team units names
func (t *Team) GetUnitNames() (res []string) {
if t.AccessMode >= perm.AccessModeAdmin {
return unit.AllUnitKeyNames()
}
for _, u := range t.Units {
res = append(res, unit.Units[u.Type].NameKey)
}
return res
}
// GetUnitsMap returns the team units permissions
func (t *Team) GetUnitsMap() map[string]string {
m := make(map[string]string)
if t.AccessMode >= perm.AccessModeAdmin {
for _, u := range unit.Units {
m[u.NameKey] = t.AccessMode.String()
}
} else {
for _, u := range t.Units {
m[u.Unit().NameKey] = u.AccessMode.String()
}
}
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
}
// LoadRepositories returns paginated repositories in team of organization.
func (t *Team) LoadRepositories(ctx context.Context) (err error) {
if t.Repos != nil {
return nil
}
t.Repos, err = GetTeamRepositories(ctx, &SearchTeamRepoOptions{
TeamID: t.ID,
})
return err
}
// 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 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 if the team has the given unit type enabled
func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode {
if err := t.LoadUnits(ctx); err != nil {
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
}
for _, unit := range t.Units {
if unit.Type == tp {
return unit.AccessMode
}
}
return 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
}
// GetTeamNamesByID returns team's lower name from a list of team ids.
func GetTeamNamesByID(ctx context.Context, teamIDs []int64) ([]string, error) {
if len(teamIDs) == 0 {
return []string{}, nil
}
var teamNames []string
err := db.GetEngine(ctx).Table("team").
Select("lower_name").
In("id", teamIDs).
Asc("name").
Find(&teamNames)
return teamNames, err
}
// 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
}
// CountInconsistentOwnerTeams returns the amount of owner teams that have all of
// their access modes set to "None".
func CountInconsistentOwnerTeams(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Table("team").
Join("INNER", "team_unit", "`team`.id = `team_unit`.team_id").
Where("`team`.lower_name = ?", strings.ToLower(OwnerTeamName)).
GroupBy("`team_unit`.team_id").
Having("SUM(`team_unit`.access_mode) = 0").
Count()
}
// FixInconsistentOwnerTeams fixes inconsistent owner teams that have all of
// their access modes set to "None", it sets it back to "Owner".
func FixInconsistentOwnerTeams(ctx context.Context) (int64, error) {
teamIDs := []int64{}
if err := db.GetEngine(ctx).Table("team").
Select("`team`.id").
Join("INNER", "team_unit", "`team`.id = `team_unit`.team_id").
Where("`team`.lower_name = ?", strings.ToLower(OwnerTeamName)).
GroupBy("`team_unit`.team_id").
Having("SUM(`team_unit`.access_mode) = 0").
Find(&teamIDs); err != nil {
return 0, err
}
if err := db.Iterate(ctx, builder.In("team_id", teamIDs), func(ctx context.Context, bean *TeamUnit) error {
if bean.Type == unit.TypeExternalTracker || bean.Type == unit.TypeExternalWiki {
bean.AccessMode = perm.AccessModeRead
} else {
bean.AccessMode = perm.AccessModeOwner
}
_, err := db.GetEngine(ctx).ID(bean.ID).Table("team_unit").Cols("access_mode").Update(bean)
return err
}); err != nil {
return 0, err
}
return int64(len(teamIDs)), nil
}