forgejo/models/unit/unit.go
limiting-factor 851356577c chore: do not leak global repository unit defaults (#11470)
The [test disabling the issue unit][0] took care of [reseting the disabled units][1]. However, it overlooked that calling [`LoadUnitConfig`][2] also has a [side effect on `DefaultRepoUnits`][3]. It happens when [`validateDefaultRepoUnits` has a side effect][4] on the array backing the slice, when it is [not recreated][5]. As a result the issue unit is disabled for all tests that run after this one.

The subtle side effect is harmless because it only happens in tests, the `LoadUnitConfig` is otherwise only called once. For clarity `LoadUnitConfig` is modified to clone the unit array being validated so that the returned slice is never backed by the same array as the argument.

As the global variables used for repository units should be saved and restored as a whole, a dedicated test function (`SaveUnits`) is provided to be used by both integration tests and unit tests. The test of the unit model is refactored to be a blackbox test in order to avoid an import cycle.

[0]: cce5f868ce/tests/integration/repo_settings_test.go (L258)
[1]: cce5f868ce/tests/integration/repo_settings_test.go (L253)
[2]: cce5f868ce/models/unit/unit.go (L171)
[3]: cce5f868ce/models/unit/unit.go (L182)
[4]: cce5f868ce/models/unit/unit.go (L162)
[5]: cce5f868ce/models/unit/unit.go (L148)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11470
Reviewed-by: Ellen Εμιλία Άννα Zscheile <fogti@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: limiting-factor <limiting-factor@posteo.com>
Co-committed-by: limiting-factor <limiting-factor@posteo.com>
2026-03-07 02:03:22 +01:00

450 lines
10 KiB
Go

// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unit
import (
"errors"
"fmt"
"slices"
"strings"
"sync/atomic"
"forgejo.org/models/perm"
"forgejo.org/modules/container"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
)
// Type is Unit's Type
type Type int
// Enumerate all the unit types
const (
TypeInvalid Type = iota // 0 invalid
TypeCode // 1 code
TypeIssues // 2 issues
TypePullRequests // 3 PRs
TypeReleases // 4 Releases
TypeWiki // 5 Wiki
TypeExternalWiki // 6 ExternalWiki
TypeExternalTracker // 7 ExternalTracker
TypeProjects // 8 Projects
TypePackages // 9 Packages
TypeActions // 10 Actions
)
// Value returns integer value for unit type
func (u Type) Value() int {
return int(u)
}
func (u Type) String() string {
switch u {
case TypeCode:
return "TypeCode"
case TypeIssues:
return "TypeIssues"
case TypePullRequests:
return "TypePullRequests"
case TypeReleases:
return "TypeReleases"
case TypeWiki:
return "TypeWiki"
case TypeExternalWiki:
return "TypeExternalWiki"
case TypeExternalTracker:
return "TypeExternalTracker"
case TypeProjects:
return "TypeProjects"
case TypePackages:
return "TypePackages"
case TypeActions:
return "TypeActions"
}
return fmt.Sprintf("Unknown Type %d", u)
}
func (u Type) LogString() string {
return fmt.Sprintf("<UnitType:%d:%s>", u, u.String())
}
var (
// AllRepoUnitTypes contains all units
AllRepoUnitTypes = []Type{
TypeCode,
TypeIssues,
TypePullRequests,
TypeReleases,
TypeWiki,
TypeExternalWiki,
TypeExternalTracker,
TypeProjects,
TypePackages,
TypeActions,
}
// DefaultRepoUnits contains default units for regular repos
DefaultRepoUnits = []Type{
TypeCode,
TypeIssues,
TypePullRequests,
TypeReleases,
TypeWiki,
TypeProjects,
TypePackages,
TypeActions,
}
// ForkRepoUnits contains default units for forks
DefaultForkRepoUnits = []Type{
TypeCode,
TypePullRequests,
}
// DefaultMirrorRepoUnits contains default units for mirrors
DefaultMirrorRepoUnits = []Type{
TypeCode,
TypeIssues,
TypeReleases,
TypeWiki,
TypeProjects,
TypePackages,
}
// NotAllowedDefaultRepoUnits contains units that can't be default
NotAllowedDefaultRepoUnits = []Type{
TypeExternalWiki,
TypeExternalTracker,
}
disabledRepoUnitsAtomic atomic.Pointer[[]Type] // the units that have been globally disabled
// AllowedRepoUnitGroups contains the units that have been globally enabled,
// with mutually exclusive units grouped together.
AllowedRepoUnitGroups = [][]Type{}
)
// DisabledRepoUnitsGet returns the globally disabled units, it is a quick patch to fix data-race during testing.
// Because the queue worker might read when a test is mocking the value. FIXME: refactor to a clear solution later.
func DisabledRepoUnitsGet() []Type {
v := disabledRepoUnitsAtomic.Load()
if v == nil {
return nil
}
return *v
}
func DisabledRepoUnitsSet(v []Type) {
disabledRepoUnitsAtomic.Store(&v)
}
// Get valid set of default repository units from settings
func validateDefaultRepoUnits(defaultUnits, settingDefaultUnits []Type) []Type {
units := slices.Clone(defaultUnits)
// Use setting if not empty
if len(settingDefaultUnits) > 0 {
units = make([]Type, 0, len(settingDefaultUnits))
for _, settingUnit := range settingDefaultUnits {
if !settingUnit.CanBeDefault() {
log.Warn("Not allowed as default unit: %s", settingUnit.String())
continue
}
units = append(units, settingUnit)
}
}
// Remove disabled units
for _, disabledUnit := range DisabledRepoUnitsGet() {
for i, unit := range units {
if unit == disabledUnit {
units = append(units[:i], units[i+1:]...)
}
}
}
return units
}
// LoadUnitConfig load units from settings
func LoadUnitConfig() error {
disabledRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DisabledRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in disabled repo units: %s", strings.Join(invalidKeys, ", "))
}
DisabledRepoUnitsSet(disabledRepoUnits)
setDefaultRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in default repo units: %s", strings.Join(invalidKeys, ", "))
}
DefaultRepoUnits = validateDefaultRepoUnits(DefaultRepoUnits, setDefaultRepoUnits)
if len(DefaultRepoUnits) == 0 {
return errors.New("no default repository units found")
}
// Default fork repo units
setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", "))
}
DefaultForkRepoUnits = validateDefaultRepoUnits(DefaultForkRepoUnits, setDefaultForkRepoUnits)
if len(DefaultForkRepoUnits) == 0 {
return errors.New("no default fork repository units found")
}
// Default mirror repo units
setDefaultMirrorRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultMirrorRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in default mirror repo units: %s", strings.Join(invalidKeys, ", "))
}
DefaultMirrorRepoUnits = validateDefaultRepoUnits(DefaultMirrorRepoUnits, setDefaultMirrorRepoUnits)
if len(DefaultMirrorRepoUnits) == 0 {
return errors.New("no default mirror repository units found")
}
// Collect the allowed repo unit groups. Mutually exclusive units are
// grouped together.
AllowedRepoUnitGroups = [][]Type{}
for _, unit := range []Type{
TypeCode,
TypePullRequests,
TypeProjects,
TypePackages,
TypeActions,
} {
// If unit is globally disabled, ignore it.
if unit.UnitGlobalDisabled() {
continue
}
// If it is allowed, add it to the group list.
AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, []Type{unit})
}
addMutuallyExclusiveGroup := func(unit1, unit2 Type) {
var list []Type
if !unit1.UnitGlobalDisabled() {
list = append(list, unit1)
}
if !unit2.UnitGlobalDisabled() {
list = append(list, unit2)
}
if len(list) > 0 {
AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, list)
}
}
addMutuallyExclusiveGroup(TypeIssues, TypeExternalTracker)
addMutuallyExclusiveGroup(TypeWiki, TypeExternalWiki)
return nil
}
// UnitGlobalDisabled checks if unit type is global disabled
func (u Type) UnitGlobalDisabled() bool {
for _, ud := range DisabledRepoUnitsGet() {
if u == ud {
return true
}
}
return false
}
// CanBeDefault checks if the unit type can be a default repo unit
func (u *Type) CanBeDefault() bool {
for _, nadU := range NotAllowedDefaultRepoUnits {
if *u == nadU {
return false
}
}
return true
}
// Unit is a section of one repository
type Unit struct {
Type Type
Name string
NameKey string
URI string
Idx int
MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read.
}
// IsLessThan compares order of two units
func (u Unit) IsLessThan(unit Unit) bool {
if (u.Type == TypeExternalTracker || u.Type == TypeExternalWiki) && unit.Type != TypeExternalTracker && unit.Type != TypeExternalWiki {
return false
}
return u.Idx < unit.Idx
}
// MaxPerm returns the max perms of this unit
func (u Unit) MaxPerm() perm.AccessMode {
if u.Type == TypeExternalTracker || u.Type == TypeExternalWiki {
return perm.AccessModeRead
}
return perm.AccessModeAdmin
}
// Enumerate all the units
var (
UnitCode = Unit{
TypeCode,
"code",
"repo.code",
"/",
0,
perm.AccessModeOwner,
}
UnitIssues = Unit{
TypeIssues,
"issues",
"repo.issues",
"/issues",
1,
perm.AccessModeOwner,
}
UnitExternalTracker = Unit{
TypeExternalTracker,
"ext_issues",
"repo.ext_issues",
"/issues",
1,
perm.AccessModeRead,
}
UnitPullRequests = Unit{
TypePullRequests,
"pulls",
"repo.pulls",
"/pulls",
2,
perm.AccessModeOwner,
}
UnitReleases = Unit{
TypeReleases,
"releases",
"repo.releases",
"/releases",
3,
perm.AccessModeOwner,
}
UnitWiki = Unit{
TypeWiki,
"wiki",
"repo.wiki",
"/wiki",
4,
perm.AccessModeOwner,
}
UnitExternalWiki = Unit{
TypeExternalWiki,
"ext_wiki",
"repo.ext_wiki",
"/wiki",
4,
perm.AccessModeRead,
}
UnitProjects = Unit{
TypeProjects,
"projects",
"repo.projects",
"/projects",
5,
perm.AccessModeOwner,
}
UnitPackages = Unit{
TypePackages,
"packages",
"repo.packages",
"/packages",
6,
perm.AccessModeRead,
}
UnitActions = Unit{
TypeActions,
"actions",
"repo.actions",
"/actions",
7,
perm.AccessModeOwner,
}
// Units contains all the units
Units = map[Type]Unit{
TypeCode: UnitCode,
TypeIssues: UnitIssues,
TypeExternalTracker: UnitExternalTracker,
TypePullRequests: UnitPullRequests,
TypeReleases: UnitReleases,
TypeWiki: UnitWiki,
TypeExternalWiki: UnitExternalWiki,
TypeProjects: UnitProjects,
TypePackages: UnitPackages,
TypeActions: UnitActions,
}
)
// FindUnitTypes give the unit key names and return valid unique units and invalid keys
func FindUnitTypes(nameKeys ...string) (res []Type, invalidKeys []string) {
m := make(container.Set[Type])
for _, key := range nameKeys {
t := TypeFromKey(key)
if t == TypeInvalid {
invalidKeys = append(invalidKeys, key)
} else if m.Add(t) {
res = append(res, t)
}
}
return res, invalidKeys
}
// TypeFromKey give the unit key name and return unit
func TypeFromKey(nameKey string) Type {
for t, u := range Units {
if strings.EqualFold(nameKey, u.NameKey) {
return t
}
}
return TypeInvalid
}
// AllUnitKeyNames returns all unit key names
func AllUnitKeyNames() []string {
res := make([]string, 0, len(Units))
for _, u := range Units {
res = append(res, u.NameKey)
}
return res
}
// MinUnitAccessMode returns the minimal permission of the permission map
func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode {
res := perm.AccessModeNone
for t, mode := range unitsMap {
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
if t == TypeExternalTracker || t == TypeExternalWiki {
continue
}
// get the minimal permission greater than AccessModeNone except all are AccessModeNone
if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) {
res = mode
}
}
return res
}