Replace #6312 Help #5833 Wiki solution for #639tags/v1.22.0-rc1
@@ -62,11 +62,13 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, | |||
return true | |||
} | |||
if len(p.Units) < 1 { | |||
// the code below depends on units to get the repository ID, not ideal but just keep it for now | |||
firstUnitRepoID := p.GetFirstUnitRepoID() | |||
if firstUnitRepoID == 0 { | |||
return false | |||
} | |||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) | |||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch) | |||
if err != nil { | |||
return false | |||
} |
@@ -582,6 +582,8 @@ var migrations = []Migration{ | |||
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), | |||
// v296 -> v297 | |||
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2), | |||
// v297 -> v298 | |||
NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -336,7 +336,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { | |||
if err != nil { | |||
return false, err | |||
} | |||
if perm.UnitsMode == nil { | |||
if len(perm.UnitsMode) == 0 { | |||
for _, u := range perm.Units { | |||
if u.Type == UnitTypeCode { | |||
return AccessModeWrite <= perm.AccessMode, nil |
@@ -0,0 +1,17 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_23 //nolint | |||
import ( | |||
"code.gitea.io/gitea/models/perm" | |||
"xorm.io/xorm" | |||
) | |||
func AddRepoUnitEveryoneAccessMode(x *xorm.Engine) error { | |||
type RepoUnit struct { //revive:disable-line:exported | |||
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` | |||
} | |||
return x.Sync(&RepoUnit{}) | |||
} |
@@ -130,11 +130,11 @@ 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() | |||
m[u.NameKey] = t.AccessMode.ToString() | |||
} | |||
} else { | |||
for _, u := range t.Units { | |||
m[u.Unit().NameKey] = u.AccessMode.String() | |||
m[u.Unit().NameKey] = u.AccessMode.ToString() | |||
} | |||
} | |||
return m |
@@ -63,13 +63,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re | |||
} | |||
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { | |||
max := perm.AccessModeNone | |||
maxMode := perm.AccessModeNone | |||
for _, mode := range modes { | |||
if mode > max { | |||
max = mode | |||
} | |||
maxMode = max(maxMode, mode) | |||
} | |||
return max | |||
return maxMode | |||
} | |||
type userAccess struct { |
@@ -6,6 +6,7 @@ package access | |||
import ( | |||
"context" | |||
"fmt" | |||
"slices" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/organization" | |||
@@ -14,13 +15,15 @@ import ( | |||
"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" | |||
) | |||
// Permission contains all the permissions related variables to a repository for a user | |||
type Permission struct { | |||
AccessMode perm_model.AccessMode | |||
Units []*repo_model.RepoUnit | |||
UnitsMode map[unit.Type]perm_model.AccessMode | |||
units []*repo_model.RepoUnit | |||
unitsMode map[unit.Type]perm_model.AccessMode | |||
} | |||
// IsOwner returns true if current user is the owner of repository. | |||
@@ -33,25 +36,44 @@ func (p *Permission) IsAdmin() bool { | |||
return p.AccessMode >= perm_model.AccessModeAdmin | |||
} | |||
// HasAccess returns true if the current user has at least read access to any unit of this repository | |||
// HasAccess returns true if the current user might have at least read access to any unit of this repository | |||
func (p *Permission) HasAccess() bool { | |||
if p.UnitsMode == nil { | |||
return p.AccessMode >= perm_model.AccessModeRead | |||
return len(p.unitsMode) > 0 || p.AccessMode >= perm_model.AccessModeRead | |||
} | |||
// HasUnits returns true if the permission contains attached units | |||
func (p *Permission) HasUnits() bool { | |||
return len(p.units) > 0 | |||
} | |||
// GetFirstUnitRepoID returns the repo ID of the first unit, it is a fragile design and should NOT be used anymore | |||
// deprecated | |||
func (p *Permission) GetFirstUnitRepoID() int64 { | |||
if len(p.units) > 0 { | |||
return p.units[0].RepoID | |||
} | |||
return len(p.UnitsMode) > 0 | |||
return 0 | |||
} | |||
// UnitAccessMode returns current user accessmode to the specify unit of the repository | |||
// UnitAccessMode returns current user access mode to the specify unit of the repository | |||
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { | |||
if p.UnitsMode == nil { | |||
for _, u := range p.Units { | |||
if u.Type == unitType { | |||
return p.AccessMode | |||
} | |||
if p.unitsMode != nil { | |||
// if the units map contains the access mode, use it, but admin/owner mode could override it | |||
if m, ok := p.unitsMode[unitType]; ok { | |||
return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) | |||
} | |||
return perm_model.AccessModeNone | |||
} | |||
return p.UnitsMode[unitType] | |||
// if the units map does not contain the access mode, return the default access mode if the unit exists | |||
hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) | |||
return util.Iif(hasUnit, p.AccessMode, perm_model.AccessModeNone) | |||
} | |||
func (p *Permission) SetUnitsWithDefaultAccessMode(units []*repo_model.RepoUnit, mode perm_model.AccessMode) { | |||
p.units = units | |||
p.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
for _, u := range p.units { | |||
p.unitsMode[u.Type] = mode | |||
} | |||
} | |||
// CanAccess returns true if user has mode access to the unit of the repository | |||
@@ -103,8 +125,8 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool { | |||
} | |||
func (p *Permission) ReadableUnitTypes() []unit.Type { | |||
types := make([]unit.Type, 0, len(p.Units)) | |||
for _, u := range p.Units { | |||
types := make([]unit.Type, 0, len(p.units)) | |||
for _, u := range p.units { | |||
if p.CanRead(u.Type) { | |||
types = append(types, u.Type) | |||
} | |||
@@ -114,21 +136,21 @@ func (p *Permission) ReadableUnitTypes() []unit.Type { | |||
func (p *Permission) LogString() string { | |||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ " | |||
args := []any{p.AccessMode.String(), len(p.Units), len(p.UnitsMode)} | |||
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)} | |||
for i, unit := range p.Units { | |||
for i, u := range p.units { | |||
config := "" | |||
if unit.Config != nil { | |||
configBytes, err := unit.Config.ToDB() | |||
if u.Config != nil { | |||
configBytes, err := u.Config.ToDB() | |||
config = string(configBytes) | |||
if err != nil { | |||
config = err.Error() | |||
} | |||
} | |||
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s" | |||
args = append(args, i, unit.ID, unit.RepoID, unit.Type.LogString(), config) | |||
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config) | |||
} | |||
for key, value := range p.UnitsMode { | |||
for key, value := range p.unitsMode { | |||
format += "\nUnitMode[%-v]: %-v" | |||
args = append(args, key.LogString(), value.LogString()) | |||
} | |||
@@ -136,23 +158,34 @@ func (p *Permission) LogString() string { | |||
return fmt.Sprintf(format, args...) | |||
} | |||
// GetUserRepoPermission returns the user permissions to the repository | |||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { | |||
var perm Permission | |||
if log.IsTrace() { | |||
defer func() { | |||
if user == nil { | |||
log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v", | |||
repo, | |||
perm) | |||
return | |||
func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { | |||
if user != nil && user.ID > 0 { | |||
for _, u := range perm.units { | |||
if perm.unitsMode == nil { | |||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
} | |||
log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v", | |||
user, | |||
repo, | |||
perm) | |||
}() | |||
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.unitsMode[u.Type] { | |||
perm.unitsMode[u.Type] = u.EveryoneAccessMode | |||
} | |||
} | |||
} | |||
} | |||
// GetUserRepoPermission returns the user permissions to the repository | |||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { | |||
defer func() { | |||
if err == nil { | |||
applyEveryoneRepoPermission(user, &perm) | |||
} | |||
if log.IsTrace() { | |||
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) | |||
} | |||
}() | |||
if err = repo.LoadUnits(ctx); err != nil { | |||
return perm, err | |||
} | |||
perm.units = repo.Units | |||
// anonymous user visit private repo. | |||
// TODO: anonymous user visit public unit of private repo??? | |||
@@ -162,7 +195,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
} | |||
var isCollaborator bool | |||
var err error | |||
if user != nil { | |||
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) | |||
if err != nil { | |||
@@ -170,7 +202,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
} | |||
} | |||
if err := repo.LoadOwner(ctx); err != nil { | |||
if err = repo.LoadOwner(ctx); err != nil { | |||
return perm, err | |||
} | |||
@@ -181,12 +213,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
return perm, nil | |||
} | |||
if err := repo.LoadUnits(ctx); err != nil { | |||
return perm, err | |||
} | |||
perm.Units = repo.Units | |||
// anonymous visit public repo | |||
if user == nil { | |||
perm.AccessMode = perm_model.AccessModeRead | |||
@@ -205,19 +231,16 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
return perm, err | |||
} | |||
if err := repo.LoadOwner(ctx); err != nil { | |||
return perm, err | |||
} | |||
if !repo.Owner.IsOrganization() { | |||
return perm, nil | |||
} | |||
perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
// Collaborators on organization | |||
if isCollaborator { | |||
for _, u := range repo.Units { | |||
perm.UnitsMode[u.Type] = perm.AccessMode | |||
perm.unitsMode[u.Type] = perm.AccessMode | |||
} | |||
} | |||
@@ -231,7 +254,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
for _, team := range teams { | |||
if team.AccessMode >= perm_model.AccessModeAdmin { | |||
perm.AccessMode = perm_model.AccessModeOwner | |||
perm.UnitsMode = nil | |||
perm.unitsMode = nil | |||
return perm, nil | |||
} | |||
} | |||
@@ -240,25 +263,25 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
var found bool | |||
for _, team := range teams { | |||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { | |||
perm.UnitsMode[u.Type] = max(perm.UnitsMode[u.Type], teamMode) | |||
perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) | |||
found = true | |||
} | |||
} | |||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units. | |||
if !found && !repo.IsPrivate && !user.IsRestricted { | |||
if _, ok := perm.UnitsMode[u.Type]; !ok { | |||
perm.UnitsMode[u.Type] = perm_model.AccessModeRead | |||
if _, ok := perm.unitsMode[u.Type]; !ok { | |||
perm.unitsMode[u.Type] = perm_model.AccessModeRead | |||
} | |||
} | |||
} | |||
// remove no permission units | |||
perm.Units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) | |||
for t := range perm.UnitsMode { | |||
perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) | |||
for t := range perm.unitsMode { | |||
for _, u := range repo.Units { | |||
if u.Type == t { | |||
perm.Units = append(perm.Units, u) | |||
perm.units = append(perm.units, u) | |||
} | |||
} | |||
} | |||
@@ -340,7 +363,7 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model. | |||
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. | |||
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) { | |||
if user.IsOrganization() { | |||
return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) | |||
return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) | |||
} | |||
perm, err := GetUserRepoPermission(ctx, repo, user) | |||
if err != nil { |
@@ -0,0 +1,98 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package access | |||
import ( | |||
"testing" | |||
perm_model "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" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestApplyEveryoneRepoPermission(t *testing.T) { | |||
perm := Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeNone}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(nil, &perm) | |||
assert.False(t, perm.CanRead(unit.TypeWiki)) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
assert.True(t, perm.CanRead(unit.TypeWiki)) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeWrite, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
assert.True(t, perm.CanRead(unit.TypeWiki)) | |||
assert.False(t, perm.CanWrite(unit.TypeWiki)) // because there is no unit mode, so the everyone-mode is used as the unit's access mode | |||
perm = Permission{ | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeWrite, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
assert.True(t, perm.CanWrite(unit.TypeWiki)) | |||
} | |||
func TestUnitAccessMode(t *testing.T) { | |||
perm := Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
} | |||
assert.Equal(t, perm_model.AccessModeNone, perm.UnitAccessMode(unit.TypeWiki), "no unit, no map, use AccessMode") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeRead, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki}, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "only unit, no map, use AccessMode") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeAdmin, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeAdmin, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, admin overrides map") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, use map") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki}, | |||
}, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") | |||
} |
@@ -5,25 +5,25 @@ package perm | |||
import ( | |||
"fmt" | |||
"slices" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
// AccessMode specifies the users access mode | |||
type AccessMode int | |||
const ( | |||
// AccessModeNone no access | |||
AccessModeNone AccessMode = iota // 0 | |||
// AccessModeRead read access | |||
AccessModeRead // 1 | |||
// AccessModeWrite write access | |||
AccessModeWrite // 2 | |||
// AccessModeAdmin admin access | |||
AccessModeAdmin // 3 | |||
// AccessModeOwner owner access | |||
AccessModeOwner // 4 | |||
AccessModeNone AccessMode = iota // 0: no access | |||
AccessModeRead // 1: read access | |||
AccessModeWrite // 2: write access | |||
AccessModeAdmin // 3: admin access | |||
AccessModeOwner // 4: owner access | |||
) | |||
func (mode AccessMode) String() string { | |||
// ToString returns the string representation of the access mode, do not make it a Stringer, otherwise it's difficult to render in templates | |||
func (mode AccessMode) ToString() string { | |||
switch mode { | |||
case AccessModeRead: | |||
return "read" | |||
@@ -39,19 +39,24 @@ func (mode AccessMode) String() string { | |||
} | |||
func (mode AccessMode) LogString() string { | |||
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.String()) | |||
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.ToString()) | |||
} | |||
// ParseAccessMode returns corresponding access mode to given permission string. | |||
func ParseAccessMode(permission string) AccessMode { | |||
func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode { | |||
m := AccessModeNone | |||
switch permission { | |||
case "read": | |||
return AccessModeRead | |||
m = AccessModeRead | |||
case "write": | |||
return AccessModeWrite | |||
m = AccessModeWrite | |||
case "admin": | |||
return AccessModeAdmin | |||
m = AccessModeAdmin | |||
default: | |||
return AccessModeNone | |||
// the "owner" access is not really used for user input, it's mainly for checking access level in code, so don't parse it | |||
} | |||
if len(allowed) == 0 { | |||
return m | |||
} | |||
return util.Iif(slices.Contains(allowed, m), m, AccessModeNone) | |||
} |
@@ -0,0 +1,22 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package perm | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestAccessMode(t *testing.T) { | |||
names := []string{"none", "read", "write", "admin"} | |||
for i, name := range names { | |||
m := ParseAccessMode(name) | |||
assert.Equal(t, AccessMode(i), m) | |||
} | |||
assert.Equal(t, AccessMode(4), AccessModeOwner) | |||
assert.Equal(t, "owner", AccessModeOwner.ToString()) | |||
assert.Equal(t, AccessModeNone, ParseAccessMode("owner")) | |||
assert.Equal(t, AccessModeNone, ParseAccessMode("invalid")) | |||
} |
@@ -10,6 +10,7 @@ import ( | |||
"strings" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/perm" | |||
"code.gitea.io/gitea/models/unit" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/setting" | |||
@@ -41,11 +42,12 @@ func (err ErrUnitTypeNotExist) Unwrap() error { | |||
// RepoUnit describes all units of a repository | |||
type RepoUnit struct { //revive:disable-line:exported | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type unit.Type `xorm:"INDEX(s)"` | |||
Config convert.Conversion `xorm:"TEXT"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type unit.Type `xorm:"INDEX(s)"` | |||
Config convert.Conversion `xorm:"TEXT"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` | |||
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` | |||
} | |||
func init() { |
@@ -191,16 +191,13 @@ type Unit struct { | |||
NameKey string | |||
URI string | |||
DescKey string | |||
Idx int | |||
Priority 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 | |||
return u.Priority < unit.Priority | |||
} | |||
// MaxPerm returns the max perms of this unit | |||
@@ -236,7 +233,7 @@ var ( | |||
"repo.ext_issues", | |||
"/issues", | |||
"repo.ext_issues.desc", | |||
1, | |||
101, | |||
perm.AccessModeRead, | |||
} | |||
@@ -272,7 +269,7 @@ var ( | |||
"repo.ext_wiki", | |||
"/wiki", | |||
"repo.ext_wiki.desc", | |||
4, | |||
102, | |||
perm.AccessModeRead, | |||
} | |||
@@ -34,6 +34,7 @@ func NewFuncMap() template.FuncMap { | |||
// ----------------------------------------------------------------- | |||
// html/template related functions | |||
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. | |||
"Iif": Iif, | |||
"Eval": Eval, | |||
"SafeHTML": SafeHTML, | |||
"HTMLFormat": HTMLFormat, | |||
@@ -238,6 +239,17 @@ func DotEscape(raw string) string { | |||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d") | |||
} | |||
// Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version, | |||
// and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal). | |||
func Iif(condition bool, vals ...any) any { | |||
if condition { | |||
return vals[0] | |||
} else if len(vals) > 1 { | |||
return vals[1] | |||
} | |||
return nil | |||
} | |||
// Eval the expression and return the result, see the comment of eval.Expr for details. | |||
// To use this helper function in templates, pass each token as a separate parameter. | |||
// |
@@ -885,6 +885,7 @@ repo_and_org_access = Repository and Organization Access | |||
permissions_public_only = Public only | |||
permissions_access_all = All (public, private, and limited) | |||
select_permissions = Select permissions | |||
permission_not_set = Not set | |||
permission_no_access = No Access | |||
permission_read = Read | |||
permission_write = Read and Write | |||
@@ -2096,6 +2097,7 @@ settings.advanced_settings = Advanced Settings | |||
settings.wiki_desc = Enable Repository Wiki | |||
settings.use_internal_wiki = Use Built-In Wiki | |||
settings.default_wiki_branch_name = Default Wiki Branch Name | |||
settings.default_wiki_everyone_access = Default Access Permission for signed-in users: | |||
settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. | |||
settings.use_external_wiki = Use External Wiki | |||
settings.external_wiki_url = External Wiki URL |
@@ -209,11 +209,7 @@ func repoAssignment() func(ctx *context.APIContext) { | |||
ctx.Error(http.StatusInternalServerError, "LoadUnits", err) | |||
return | |||
} | |||
ctx.Repo.Permission.Units = ctx.Repo.Repository.Units | |||
ctx.Repo.Permission.UnitsMode = make(map[unit.Type]perm.AccessMode) | |||
for _, u := range ctx.Repo.Repository.Units { | |||
ctx.Repo.Permission.UnitsMode[u.Type] = ctx.Repo.Permission.AccessMode | |||
} | |||
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) | |||
} else { | |||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | |||
if err != nil { |
@@ -481,11 +481,7 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool { | |||
}) | |||
return false | |||
} | |||
ctx.userPerm.Units = ctx.Repo.Repository.Units | |||
ctx.userPerm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
for _, u := range ctx.Repo.Repository.Units { | |||
ctx.userPerm.UnitsMode[u.Type] = ctx.userPerm.AccessMode | |||
} | |||
ctx.userPerm.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.userPerm.AccessMode) | |||
} else { | |||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) | |||
if err != nil { |
@@ -16,6 +16,7 @@ import ( | |||
actions_model "code.gitea.io/gitea/models/actions" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/organization" | |||
"code.gitea.io/gitea/models/perm" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
unit_model "code.gitea.io/gitea/models/unit" | |||
user_model "code.gitea.io/gitea/models/user" | |||
@@ -476,9 +477,10 @@ func SettingsPost(ctx *context.Context) { | |||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) | |||
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { | |||
units = append(units, repo_model.RepoUnit{ | |||
RepoID: repo.ID, | |||
Type: unit_model.TypeWiki, | |||
Config: new(repo_model.UnitConfig), | |||
RepoID: repo.ID, | |||
Type: unit_model.TypeWiki, | |||
Config: new(repo_model.UnitConfig), | |||
EveryoneAccessMode: perm.ParseAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite), | |||
}) | |||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) | |||
} else { |
@@ -684,7 +684,7 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i | |||
} | |||
func checkHomeCodeViewable(ctx *context.Context) { | |||
if len(ctx.Repo.Units) > 0 { | |||
if ctx.Repo.HasUnits() { | |||
if ctx.Repo.Repository.IsBeingCreated() { | |||
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID) | |||
if err != nil { | |||
@@ -723,6 +723,7 @@ func checkHomeCodeViewable(ctx *context.Context) { | |||
var firstUnit *unit_model.Unit | |||
for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() { | |||
if repoUnitType == unit_model.TypeCode { | |||
// we are doing this check in "code" unit related pages, so if the code unit is readable, no need to do any further redirection | |||
return | |||
} | |||
@@ -336,7 +336,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([] | |||
Description: t.Description, | |||
IncludesAllRepositories: t.IncludesAllRepositories, | |||
CanCreateOrgRepo: t.CanCreateOrgRepo, | |||
Permission: t.AccessMode.String(), | |||
Permission: t.AccessMode.ToString(), | |||
Units: t.GetUnitNames(), | |||
UnitsMap: t.GetUnitsMap(), | |||
} |
@@ -25,12 +25,13 @@ func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo a | |||
func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { | |||
var parent *api.Repository | |||
if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil { | |||
// If Units and UnitsMode are both nil, it means that it's a hard coded permission, | |||
// like access_model.Permission{AccessMode: perm.AccessModeAdmin}. | |||
// So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone. | |||
if !permissionInRepo.HasUnits() && permissionInRepo.AccessMode > perm.AccessModeNone { | |||
// If units is empty, it means that it's a hard-coded permission, like access_model.Permission{AccessMode: perm.AccessModeAdmin} | |||
// So we need to load units for the repo, otherwise UnitAccessMode will just return perm.AccessModeNone. | |||
// TODO: this logic is still not right (because unit modes are not correctly prepared) | |||
// the caller should prepare a proper "permission" before calling this function. | |||
_ = repo.LoadUnits(ctx) // the error is not important, so ignore it | |||
permissionInRepo.Units = repo.Units | |||
permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode) | |||
} | |||
cloneLink := repo.CloneLink() |
@@ -103,7 +103,7 @@ func User2UserSettings(user *user_model.User) api.UserSettings { | |||
func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission { | |||
return api.RepoCollaboratorPermission{ | |||
User: ToUser(ctx, user, doer), | |||
Permission: accessMode.String(), | |||
RoleName: accessMode.String(), | |||
Permission: accessMode.ToString(), | |||
RoleName: accessMode.ToString(), | |||
} | |||
} |
@@ -134,6 +134,7 @@ type RepoSettingForm struct { | |||
EnableWiki bool | |||
EnableExternalWiki bool | |||
DefaultWikiBranch string | |||
DefaultWikiEveryoneAccess string | |||
ExternalWikiURL string | |||
EnableIssues bool | |||
EnableExternalTracker bool |
@@ -317,7 +317,9 @@ | |||
</div> | |||
</div> | |||
{{$isWikiEnabled := or (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeWiki) (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}} | |||
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}} | |||
{{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}} | |||
{{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}} | |||
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}} | |||
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}} | |||
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}} | |||
@@ -331,21 +333,33 @@ | |||
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box"> | |||
<div class="field"> | |||
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> | |||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-target="#external_wiki_box" {{if not (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}}checked{{end}}> | |||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}> | |||
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label> | |||
</div> | |||
</div> | |||
<div class="inline field tw-pl-4"> | |||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label> | |||
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}"> | |||
<div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}"> | |||
<div class="inline field"> | |||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label> | |||
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}"> | |||
</div> | |||
<div class="inline field"> | |||
{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} | |||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label> | |||
<select name="default_wiki_everyone_access" class="ui dropdown"> | |||
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}} | |||
<option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> | |||
<option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> | |||
<option value="write" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 2) "selected"}}>{{ctx.Locale.Tr "settings.permission_write"}}</option> | |||
</select> | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> | |||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki}}checked{{end}}> | |||
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}> | |||
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label> | |||
</div> | |||
</div> | |||
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box"> | |||
<div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}"> | |||
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label> | |||
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}"> | |||
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p> |
@@ -126,7 +126,7 @@ func TestAPITeam(t *testing.T) { | |||
apiTeam = api.Team{} | |||
DecodeJSON(t, resp, &apiTeam) | |||
checkTeamResponse(t, "ReadTeam1", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, | |||
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap()) | |||
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap()) | |||
// Delete team. | |||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). | |||
@@ -197,7 +197,7 @@ func TestAPITeam(t *testing.T) { | |||
DecodeJSON(t, resp, &apiTeam) | |||
assert.NoError(t, teamRead.LoadUnits(db.DefaultContext)) | |||
checkTeamResponse(t, "ReadTeam2", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, | |||
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap()) | |||
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap()) | |||
// Delete team. | |||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). |