Replace #6312 Help #5833 Wiki solution for #639tags/v1.22.0-rc1
return true | 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 | return false | ||||
} | } | ||||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) | |||||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch) | |||||
if err != nil { | if err != nil { | ||||
return false | return false | ||||
} | } |
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), | NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), | ||||
// v296 -> v297 | // v296 -> v297 | ||||
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2), | 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 | // GetCurrentDBVersion returns the current db version |
if err != nil { | if err != nil { | ||||
return false, err | return false, err | ||||
} | } | ||||
if perm.UnitsMode == nil { | |||||
if len(perm.UnitsMode) == 0 { | |||||
for _, u := range perm.Units { | for _, u := range perm.Units { | ||||
if u.Type == UnitTypeCode { | if u.Type == UnitTypeCode { | ||||
return AccessModeWrite <= perm.AccessMode, nil | return AccessModeWrite <= perm.AccessMode, nil |
// 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{}) | |||||
} |
m := make(map[string]string) | m := make(map[string]string) | ||||
if t.AccessMode >= perm.AccessModeAdmin { | if t.AccessMode >= perm.AccessModeAdmin { | ||||
for _, u := range unit.Units { | for _, u := range unit.Units { | ||||
m[u.NameKey] = t.AccessMode.String() | |||||
m[u.NameKey] = t.AccessMode.ToString() | |||||
} | } | ||||
} else { | } else { | ||||
for _, u := range t.Units { | for _, u := range t.Units { | ||||
m[u.Unit().NameKey] = u.AccessMode.String() | |||||
m[u.Unit().NameKey] = u.AccessMode.ToString() | |||||
} | } | ||||
} | } | ||||
return m | return m |
} | } | ||||
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { | func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { | ||||
max := perm.AccessModeNone | |||||
maxMode := perm.AccessModeNone | |||||
for _, mode := range modes { | for _, mode := range modes { | ||||
if mode > max { | |||||
max = mode | |||||
} | |||||
maxMode = max(maxMode, mode) | |||||
} | } | ||||
return max | |||||
return maxMode | |||||
} | } | ||||
type userAccess struct { | type userAccess struct { |
import ( | import ( | ||||
"context" | "context" | ||||
"fmt" | "fmt" | ||||
"slices" | |||||
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
"code.gitea.io/gitea/models/organization" | "code.gitea.io/gitea/models/organization" | ||||
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
"code.gitea.io/gitea/modules/log" | "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 | // Permission contains all the permissions related variables to a repository for a user | ||||
type Permission struct { | type Permission struct { | ||||
AccessMode perm_model.AccessMode | 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. | // IsOwner returns true if current user is the owner of repository. | ||||
return p.AccessMode >= perm_model.AccessModeAdmin | 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 { | 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 { | 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 | // CanAccess returns true if user has mode access to the unit of the repository | ||||
} | } | ||||
func (p *Permission) ReadableUnitTypes() []unit.Type { | 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) { | if p.CanRead(u.Type) { | ||||
types = append(types, u.Type) | types = append(types, u.Type) | ||||
} | } | ||||
func (p *Permission) LogString() string { | func (p *Permission) LogString() string { | ||||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ " | 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 := "" | config := "" | ||||
if unit.Config != nil { | |||||
configBytes, err := unit.Config.ToDB() | |||||
if u.Config != nil { | |||||
configBytes, err := u.Config.ToDB() | |||||
config = string(configBytes) | config = string(configBytes) | ||||
if err != nil { | if err != nil { | ||||
config = err.Error() | config = err.Error() | ||||
} | } | ||||
} | } | ||||
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s" | 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" | format += "\nUnitMode[%-v]: %-v" | ||||
args = append(args, key.LogString(), value.LogString()) | args = append(args, key.LogString(), value.LogString()) | ||||
} | } | ||||
return fmt.Sprintf(format, args...) | 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. | // anonymous user visit private repo. | ||||
// TODO: anonymous user visit public unit of private repo??? | // TODO: anonymous user visit public unit of private repo??? | ||||
} | } | ||||
var isCollaborator bool | var isCollaborator bool | ||||
var err error | |||||
if user != nil { | if user != nil { | ||||
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) | isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) | ||||
if err != nil { | if err != nil { | ||||
} | } | ||||
} | } | ||||
if err := repo.LoadOwner(ctx); err != nil { | |||||
if err = repo.LoadOwner(ctx); err != nil { | |||||
return perm, err | return perm, err | ||||
} | } | ||||
return perm, nil | return perm, nil | ||||
} | } | ||||
if err := repo.LoadUnits(ctx); err != nil { | |||||
return perm, err | |||||
} | |||||
perm.Units = repo.Units | |||||
// anonymous visit public repo | // anonymous visit public repo | ||||
if user == nil { | if user == nil { | ||||
perm.AccessMode = perm_model.AccessModeRead | perm.AccessMode = perm_model.AccessModeRead | ||||
return perm, err | return perm, err | ||||
} | } | ||||
if err := repo.LoadOwner(ctx); err != nil { | |||||
return perm, err | |||||
} | |||||
if !repo.Owner.IsOrganization() { | if !repo.Owner.IsOrganization() { | ||||
return perm, nil | return perm, nil | ||||
} | } | ||||
perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) | |||||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||||
// Collaborators on organization | // Collaborators on organization | ||||
if isCollaborator { | if isCollaborator { | ||||
for _, u := range repo.Units { | for _, u := range repo.Units { | ||||
perm.UnitsMode[u.Type] = perm.AccessMode | |||||
perm.unitsMode[u.Type] = perm.AccessMode | |||||
} | } | ||||
} | } | ||||
for _, team := range teams { | for _, team := range teams { | ||||
if team.AccessMode >= perm_model.AccessModeAdmin { | if team.AccessMode >= perm_model.AccessModeAdmin { | ||||
perm.AccessMode = perm_model.AccessModeOwner | perm.AccessMode = perm_model.AccessModeOwner | ||||
perm.UnitsMode = nil | |||||
perm.unitsMode = nil | |||||
return perm, nil | return perm, nil | ||||
} | } | ||||
} | } | ||||
var found bool | var found bool | ||||
for _, team := range teams { | for _, team := range teams { | ||||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { | 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 | found = true | ||||
} | } | ||||
} | } | ||||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units. | // 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 !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 | // 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 { | for _, u := range repo.Units { | ||||
if u.Type == t { | if u.Type == t { | ||||
perm.Units = append(perm.Units, u) | |||||
perm.units = append(perm.units, u) | |||||
} | } | ||||
} | } | ||||
} | } | ||||
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. | // 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) { | func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) { | ||||
if user.IsOrganization() { | 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) | perm, err := GetUserRepoPermission(ctx, repo, user) | ||||
if err != nil { | if err != nil { |
// 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") | |||||
} |
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"slices" | |||||
"code.gitea.io/gitea/modules/util" | |||||
) | ) | ||||
// AccessMode specifies the users access mode | // AccessMode specifies the users access mode | ||||
type AccessMode int | type AccessMode int | ||||
const ( | 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 { | switch mode { | ||||
case AccessModeRead: | case AccessModeRead: | ||||
return "read" | return "read" | ||||
} | } | ||||
func (mode AccessMode) LogString() 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. | // 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 { | switch permission { | ||||
case "read": | case "read": | ||||
return AccessModeRead | |||||
m = AccessModeRead | |||||
case "write": | case "write": | ||||
return AccessModeWrite | |||||
m = AccessModeWrite | |||||
case "admin": | case "admin": | ||||
return AccessModeAdmin | |||||
m = AccessModeAdmin | |||||
default: | 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) | |||||
} | } |
// 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")) | |||||
} |
"strings" | "strings" | ||||
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
"code.gitea.io/gitea/models/perm" | |||||
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
"code.gitea.io/gitea/modules/json" | "code.gitea.io/gitea/modules/json" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
// RepoUnit describes all units of a repository | // RepoUnit describes all units of a repository | ||||
type RepoUnit struct { //revive:disable-line:exported | 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() { | func init() { |
NameKey string | NameKey string | ||||
URI string | URI string | ||||
DescKey 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. | 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 | // IsLessThan compares order of two units | ||||
func (u Unit) IsLessThan(unit Unit) bool { | 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 | // MaxPerm returns the max perms of this unit | ||||
"repo.ext_issues", | "repo.ext_issues", | ||||
"/issues", | "/issues", | ||||
"repo.ext_issues.desc", | "repo.ext_issues.desc", | ||||
1, | |||||
101, | |||||
perm.AccessModeRead, | perm.AccessModeRead, | ||||
} | } | ||||
"repo.ext_wiki", | "repo.ext_wiki", | ||||
"/wiki", | "/wiki", | ||||
"repo.ext_wiki.desc", | "repo.ext_wiki.desc", | ||||
4, | |||||
102, | |||||
perm.AccessModeRead, | perm.AccessModeRead, | ||||
} | } | ||||
// ----------------------------------------------------------------- | // ----------------------------------------------------------------- | ||||
// html/template related functions | // html/template related functions | ||||
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. | "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. | ||||
"Iif": Iif, | |||||
"Eval": Eval, | "Eval": Eval, | ||||
"SafeHTML": SafeHTML, | "SafeHTML": SafeHTML, | ||||
"HTMLFormat": HTMLFormat, | "HTMLFormat": HTMLFormat, | ||||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d") | 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. | // 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. | // To use this helper function in templates, pass each token as a separate parameter. | ||||
// | // |
permissions_public_only = Public only | permissions_public_only = Public only | ||||
permissions_access_all = All (public, private, and limited) | permissions_access_all = All (public, private, and limited) | ||||
select_permissions = Select permissions | select_permissions = Select permissions | ||||
permission_not_set = Not set | |||||
permission_no_access = No Access | permission_no_access = No Access | ||||
permission_read = Read | permission_read = Read | ||||
permission_write = Read and Write | permission_write = Read and Write | ||||
settings.wiki_desc = Enable Repository Wiki | settings.wiki_desc = Enable Repository Wiki | ||||
settings.use_internal_wiki = Use Built-In Wiki | settings.use_internal_wiki = Use Built-In Wiki | ||||
settings.default_wiki_branch_name = Default Wiki Branch Name | 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.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. | ||||
settings.use_external_wiki = Use External Wiki | settings.use_external_wiki = Use External Wiki | ||||
settings.external_wiki_url = External Wiki URL | settings.external_wiki_url = External Wiki URL |
ctx.Error(http.StatusInternalServerError, "LoadUnits", err) | ctx.Error(http.StatusInternalServerError, "LoadUnits", err) | ||||
return | 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 { | } else { | ||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | ||||
if err != nil { | if err != nil { |
}) | }) | ||||
return false | 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 { | } else { | ||||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) | user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) | ||||
if err != nil { | if err != nil { |
actions_model "code.gitea.io/gitea/models/actions" | actions_model "code.gitea.io/gitea/models/actions" | ||||
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
"code.gitea.io/gitea/models/organization" | "code.gitea.io/gitea/models/organization" | ||||
"code.gitea.io/gitea/models/perm" | |||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
unit_model "code.gitea.io/gitea/models/unit" | unit_model "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) | deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) | ||||
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { | } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { | ||||
units = append(units, repo_model.RepoUnit{ | 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) | deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) | ||||
} else { | } else { |
} | } | ||||
func checkHomeCodeViewable(ctx *context.Context) { | func checkHomeCodeViewable(ctx *context.Context) { | ||||
if len(ctx.Repo.Units) > 0 { | |||||
if ctx.Repo.HasUnits() { | |||||
if ctx.Repo.Repository.IsBeingCreated() { | if ctx.Repo.Repository.IsBeingCreated() { | ||||
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID) | task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID) | ||||
if err != nil { | if err != nil { | ||||
var firstUnit *unit_model.Unit | var firstUnit *unit_model.Unit | ||||
for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() { | for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() { | ||||
if repoUnitType == unit_model.TypeCode { | 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 | return | ||||
} | } | ||||
Description: t.Description, | Description: t.Description, | ||||
IncludesAllRepositories: t.IncludesAllRepositories, | IncludesAllRepositories: t.IncludesAllRepositories, | ||||
CanCreateOrgRepo: t.CanCreateOrgRepo, | CanCreateOrgRepo: t.CanCreateOrgRepo, | ||||
Permission: t.AccessMode.String(), | |||||
Permission: t.AccessMode.ToString(), | |||||
Units: t.GetUnitNames(), | Units: t.GetUnitNames(), | ||||
UnitsMap: t.GetUnitsMap(), | UnitsMap: t.GetUnitsMap(), | ||||
} | } |
func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { | func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { | ||||
var parent *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 | _ = repo.LoadUnits(ctx) // the error is not important, so ignore it | ||||
permissionInRepo.Units = repo.Units | |||||
permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode) | |||||
} | } | ||||
cloneLink := repo.CloneLink() | cloneLink := repo.CloneLink() |
func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission { | func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission { | ||||
return api.RepoCollaboratorPermission{ | return api.RepoCollaboratorPermission{ | ||||
User: ToUser(ctx, user, doer), | User: ToUser(ctx, user, doer), | ||||
Permission: accessMode.String(), | |||||
RoleName: accessMode.String(), | |||||
Permission: accessMode.ToString(), | |||||
RoleName: accessMode.ToString(), | |||||
} | } | ||||
} | } |
EnableWiki bool | EnableWiki bool | ||||
EnableExternalWiki bool | EnableExternalWiki bool | ||||
DefaultWikiBranch string | DefaultWikiBranch string | ||||
DefaultWikiEveryoneAccess string | |||||
ExternalWikiURL string | ExternalWikiURL string | ||||
EnableIssues bool | EnableIssues bool | ||||
EnableExternalTracker bool | EnableExternalTracker bool |
</div> | </div> | ||||
</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}} | {{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}} | ||||
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}} | {{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}} | ||||
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}} | {{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}} | ||||
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box"> | <div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box"> | ||||
<div class="field"> | <div class="field"> | ||||
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> | <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> | <label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label> | ||||
</div> | </div> | ||||
</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> | ||||
<div class="field"> | <div class="field"> | ||||
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> | <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> | <label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label> | ||||
</div> | </div> | ||||
</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> | <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}}"> | <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> | <p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p> |
apiTeam = api.Team{} | apiTeam = api.Team{} | ||||
DecodeJSON(t, resp, &apiTeam) | DecodeJSON(t, resp, &apiTeam) | ||||
checkTeamResponse(t, "ReadTeam1", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, | 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. | // Delete team. | ||||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). | req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). | ||||
DecodeJSON(t, resp, &apiTeam) | DecodeJSON(t, resp, &apiTeam) | ||||
assert.NoError(t, teamRead.LoadUnits(db.DefaultContext)) | assert.NoError(t, teamRead.LoadUnits(db.DefaultContext)) | ||||
checkTeamResponse(t, "ReadTeam2", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, | 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. | // Delete team. | ||||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). | req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID). |