diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2024-04-17 23:58:37 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-17 15:58:37 +0000 |
commit | 3feba9f1f44156c256a30d25ad1c25f751819c94 (patch) | |
tree | 77d9575edc49e625b54f5358d67dba0764b1a553 /models/perm | |
parent | bafb80f80d5505b03e5994d1ea6e2dab10052fe1 (diff) | |
download | gitea-3feba9f1f44156c256a30d25ad1c25f751819c94.tar.gz gitea-3feba9f1f44156c256a30d25ad1c25f751819c94.zip |
Allow everyone to read or write a wiki by a repo unit setting (#30495)
Replace #6312
Help #5833
Wiki solution for #639
Diffstat (limited to 'models/perm')
-rw-r--r-- | models/perm/access/access.go | 8 | ||||
-rw-r--r-- | models/perm/access/repo_permission.go | 139 | ||||
-rw-r--r-- | models/perm/access/repo_permission_test.go | 98 | ||||
-rw-r--r-- | models/perm/access_mode.go | 39 | ||||
-rw-r--r-- | models/perm/access_mode_test.go | 22 |
5 files changed, 226 insertions, 80 deletions
diff --git a/models/perm/access/access.go b/models/perm/access/access.go index b422a08614..6a0a901f71 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -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 { diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index e4e7579e62..9cce95b776 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -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 { diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go new file mode 100644 index 0000000000..aaa53bb24f --- /dev/null +++ b/models/perm/access/repo_permission_test.go @@ -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") +} diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go index a37bc1f0e1..0364191e2e 100644 --- a/models/perm/access_mode.go +++ b/models/perm/access_mode.go @@ -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) } diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go new file mode 100644 index 0000000000..982fceee5a --- /dev/null +++ b/models/perm/access_mode_test.go @@ -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")) +} |