Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion models/migrations/v1_11/v111.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v1_23/v297.go
Original file line number Diff line number Diff line change
@@ -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 -1"`
}
return x.Sync(&RepoUnit{})
}
4 changes: 2 additions & 2 deletions models/organization/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 29 additions & 37 deletions models/perm/access/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
type Permission struct {
AccessMode perm_model.AccessMode
Units []*repo_model.RepoUnit
UnitsMode map[unit.Type]perm_model.AccessMode
UnitsMode map[unit.Type]perm_model.AccessMode // zero length means use the AccessMode above
}

// IsOwner returns true if current user is the owner of repository.
Expand All @@ -33,17 +33,14 @@ 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
return len(p.UnitsMode) > 0 || p.AccessMode >= perm_model.AccessModeRead
}

// 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 {
if len(p.UnitsMode) == 0 {
for _, u := range p.Units {
if u.Type == unitType {
return p.AccessMode
Expand Down Expand Up @@ -104,7 +101,7 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {

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 {
config := ""
Expand All @@ -126,23 +123,30 @@ 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 applyDefaultUserRepoPermission(user *user_model.User, perm *Permission) {
if user != nil && user.ID > 0 {
for _, u := range perm.Units {
if u.EveryoneAccessMode > 0 && u.EveryoneAccessMode > perm.UnitsMode[u.Type] {
perm.UnitsMode[u.Type] = u.EveryoneAccessMode
}
log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v",
user,
repo,
perm)
}()
}
}
}

// 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() {
applyDefaultUserRepoPermission(user, &perm)
if log.IsTrace() {
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
}
}()

perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) // always initialize UnitsMode to avoid nil panic
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???
Expand All @@ -152,15 +156,14 @@ 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 {
return perm, err
}
}

if err := repo.LoadOwner(ctx); err != nil {
if err = repo.LoadOwner(ctx); err != nil {
return perm, err
}

Expand All @@ -171,12 +174,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
Expand All @@ -195,15 +192,10 @@ 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)

// Collaborators on organization
if isCollaborator {
for _, u := range repo.Units {
Expand Down
69 changes: 69 additions & 0 deletions models/perm/access/repo_permission_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 TestApplyDefaultUserRepoPermission(t *testing.T) {
perm := Permission{
AccessMode: perm_model.AccessModeNone,
UnitsMode: map[unit.Type]perm_model.AccessMode{},
}

perm.Units = []*repo_model.RepoUnit{
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeNone},
}
applyDefaultUserRepoPermission(nil, &perm)
assert.False(t, perm.CanRead(unit.TypeWiki))

perm.Units = []*repo_model.RepoUnit{
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}
applyDefaultUserRepoPermission(&user_model.User{ID: 1}, &perm)
assert.True(t, perm.CanRead(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 or map, use AccessMode")

perm = Permission{
AccessMode: perm_model.AccessModeOwner,
Units: []*repo_model.RepoUnit{
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
},
UnitsMode: map[unit.Type]perm_model.AccessMode{},
}
assert.Equal(t, perm_model.AccessModeOwner, perm.UnitAccessMode(unit.TypeWiki), "only unit no map, use AccessMode")

perm = Permission{
AccessMode: perm_model.AccessModeOwner,
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.AccessModeOwner,
Units: []*repo_model.RepoUnit{
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeWrite},
},
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")
}
44 changes: 27 additions & 17 deletions models/perm/access_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,29 @@ 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
AccessModeUnset AccessMode = -1 + iota // -1: no access mode is set

AccessModeNone // 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 AccessModeUnset:
return "unset"
case AccessModeRead:
return "read"
case AccessModeWrite:
Expand All @@ -39,19 +42,26 @@ 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 "unset":
m = AccessModeUnset
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)
}
21 changes: 21 additions & 0 deletions models/perm/access_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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{ /*-1*/ "unset", "none", "read", "write", "admin"}
for i, name := range names {
m := ParseAccessMode(name)
assert.Equal(t, AccessMode(i-1), m)
}
assert.Equal(t, "owner", AccessModeOwner.ToString())
assert.Equal(t, AccessModeNone, ParseAccessMode("owner"))
assert.Equal(t, AccessModeNone, ParseAccessMode("invalid"))
}
12 changes: 7 additions & 5 deletions models/repo/repo_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 -1"`
}

func init() {
Expand Down
10 changes: 10 additions & 0 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,6 +239,15 @@ func DotEscape(raw string) string {
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
}

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.
//
Expand Down
Loading