first-commit
This commit is contained in:
248
models/perm/access/access.go
Normal file
248
models/perm/access/access.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Access represents the highest access level of a user to the repository. The only access type
|
||||
// that is not in this table is the real owner of a repository. In case of an organization
|
||||
// repository, the members of the owners team are in this table.
|
||||
type Access struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"UNIQUE(s)"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
Mode perm.AccessMode
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Access))
|
||||
}
|
||||
|
||||
func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm.AccessMode, error) {
|
||||
mode := perm.AccessModeNone
|
||||
var userID int64
|
||||
restricted := false
|
||||
|
||||
if user != nil {
|
||||
userID = user.ID
|
||||
restricted = user.IsRestricted
|
||||
}
|
||||
|
||||
if !restricted && !repo.IsPrivate {
|
||||
mode = perm.AccessModeRead
|
||||
}
|
||||
|
||||
if userID == 0 {
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
if userID == repo.OwnerID {
|
||||
return perm.AccessModeOwner, nil
|
||||
}
|
||||
|
||||
a, exist, err := db.Get[Access](ctx, builder.Eq{"user_id": userID, "repo_id": repo.ID})
|
||||
if err != nil {
|
||||
return mode, err
|
||||
} else if !exist {
|
||||
return mode, nil
|
||||
}
|
||||
return a.Mode, nil
|
||||
}
|
||||
|
||||
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode {
|
||||
maxMode := perm.AccessModeNone
|
||||
for _, mode := range modes {
|
||||
maxMode = max(maxMode, mode)
|
||||
}
|
||||
return maxMode
|
||||
}
|
||||
|
||||
type userAccess struct {
|
||||
User *user_model.User
|
||||
Mode perm.AccessMode
|
||||
}
|
||||
|
||||
// updateUserAccess updates an access map so that user has at least mode
|
||||
func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mode perm.AccessMode) {
|
||||
if ua, ok := accessMap[user.ID]; ok {
|
||||
ua.Mode = maxAccessMode(ua.Mode, mode)
|
||||
} else {
|
||||
accessMap[user.ID] = &userAccess{User: user, Mode: mode}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
|
||||
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
|
||||
minMode := perm.AccessModeRead
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
// If the repo isn't private and isn't owned by a organization,
|
||||
// increase the minMode to Write.
|
||||
if !repo.IsPrivate && !repo.Owner.IsOrganization() {
|
||||
minMode = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
newAccesses := make([]Access, 0, len(accessMap))
|
||||
for userID, ua := range accessMap {
|
||||
if ua.Mode < minMode && !ua.User.IsRestricted {
|
||||
continue
|
||||
}
|
||||
|
||||
newAccesses = append(newAccesses, Access{
|
||||
UserID: userID,
|
||||
RepoID: repo.ID,
|
||||
Mode: ua.Mode,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete old accesses and insert new ones for repository.
|
||||
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
|
||||
return fmt.Errorf("delete old accesses: %w", err)
|
||||
}
|
||||
if len(newAccesses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, newAccesses); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
|
||||
func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
|
||||
collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetCollaborators: %w", err)
|
||||
}
|
||||
for _, c := range collaborators {
|
||||
if c.User.IsGhost() {
|
||||
continue
|
||||
}
|
||||
updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecalculateTeamAccesses recalculates new accesses for teams of an organization
|
||||
// except the team whose ID is given. It is used to assign a team ID when
|
||||
// remove repository from that team.
|
||||
func RecalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, ignTeamID int64) (err error) {
|
||||
accessMap := make(map[int64]*userAccess, 20)
|
||||
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
} else if !repo.Owner.IsOrganization() {
|
||||
return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
|
||||
}
|
||||
|
||||
if err = refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil {
|
||||
return fmt.Errorf("refreshCollaboratorAccesses: %w", err)
|
||||
}
|
||||
|
||||
teams, err := organization.FindOrgTeams(ctx, repo.Owner.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range teams {
|
||||
if t.ID == ignTeamID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Owner team gets owner access, and skip for teams that do not
|
||||
// have relations with repository.
|
||||
if t.IsOwnerTeam() {
|
||||
t.AccessMode = perm.AccessModeOwner
|
||||
} else if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = t.LoadMembers(ctx); err != nil {
|
||||
return fmt.Errorf("getMembers '%d': %w", t.ID, err)
|
||||
}
|
||||
for _, m := range t.Members {
|
||||
updateUserAccess(accessMap, m, t.AccessMode)
|
||||
}
|
||||
}
|
||||
|
||||
return refreshAccesses(ctx, repo, accessMap)
|
||||
}
|
||||
|
||||
// RecalculateUserAccess recalculates new access for a single user
|
||||
// Usable if we know access only affected one user
|
||||
func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
|
||||
minMode := perm.AccessModeRead
|
||||
if !repo.IsPrivate {
|
||||
minMode = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
accessMode := perm.AccessModeNone
|
||||
e := db.GetEngine(ctx)
|
||||
collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if collaborator != nil {
|
||||
accessMode = collaborator.Mode
|
||||
}
|
||||
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
} else if repo.Owner.IsOrganization() {
|
||||
var teams []organization.Team
|
||||
if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team.org_id = ?", repo.OwnerID).
|
||||
And("team_repo.repo_id=?", repo.ID).
|
||||
And("team_user.uid=?", uid).
|
||||
Find(&teams); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range teams {
|
||||
if t.IsOwnerTeam() {
|
||||
t.AccessMode = perm.AccessModeOwner
|
||||
}
|
||||
|
||||
accessMode = maxAccessMode(accessMode, t.AccessMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old user accesses and insert new one for repository.
|
||||
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil {
|
||||
return fmt.Errorf("delete old user accesses: %w", err)
|
||||
} else if accessMode >= minMode {
|
||||
if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil {
|
||||
return fmt.Errorf("insert new user accesses: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecalculateAccesses recalculates all accesses for repository.
|
||||
func RecalculateAccesses(ctx context.Context, repo *repo_model.Repository) error {
|
||||
if repo.Owner.IsOrganization() {
|
||||
return RecalculateTeamAccesses(ctx, repo, 0)
|
||||
}
|
||||
|
||||
accessMap := make(map[int64]*userAccess, 20)
|
||||
if err := refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil {
|
||||
return fmt.Errorf("refreshCollaboratorAccesses: %w", err)
|
||||
}
|
||||
return refreshAccesses(ctx, repo, accessMap)
|
||||
}
|
126
models/perm/access/access_test.go
Normal file
126
models/perm/access/access_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccessLevel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
// A public repository owned by User 2
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
assert.False(t, repo1.IsPrivate)
|
||||
// A private repository owned by Org 3
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, repo3.IsPrivate)
|
||||
|
||||
// Another public repository
|
||||
repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.False(t, repo4.IsPrivate)
|
||||
// org. owned private repo
|
||||
repo24 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
|
||||
|
||||
level, err := access_model.AccessLevel(db.DefaultContext, user2, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeOwner, level)
|
||||
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user2, repo3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeOwner, level)
|
||||
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user5, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, level)
|
||||
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user5, repo3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, level)
|
||||
|
||||
// restricted user has no access to a public repo
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user29, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, level)
|
||||
|
||||
// ... unless he's a collaborator
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user29, repo4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, level)
|
||||
|
||||
// ... or a team member
|
||||
level, err = access_model.AccessLevel(db.DefaultContext, user29, repo24)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, level)
|
||||
}
|
||||
|
||||
func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
// A public repository owned by User 2
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
assert.False(t, repo1.IsPrivate)
|
||||
// A private repository owned by Org 3
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, repo2.IsPrivate)
|
||||
|
||||
has, err := access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
|
||||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
// test with organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.NoError(t, repo1.LoadOwner(db.DefaultContext))
|
||||
|
||||
_, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1))
|
||||
|
||||
access := &access_model.Access{UserID: 2, RepoID: 3}
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(access)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, perm_model.AccessModeOwner, access.Mode)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
// test with non-organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo1.LoadOwner(db.DefaultContext))
|
||||
|
||||
_, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1))
|
||||
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 4, RepoID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
}
|
20
models/perm/access/main_test.go
Normal file
20
models/perm/access/main_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
_ "code.gitea.io/gitea/models/activities"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
_ "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
529
models/perm/access/repo_permission.go
Normal file
529
models/perm/access/repo_permission.go
Normal file
@@ -0,0 +1,529 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
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"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"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
|
||||
|
||||
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
|
||||
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
|
||||
}
|
||||
|
||||
// IsOwner returns true if current user is the owner of repository.
|
||||
func (p *Permission) IsOwner() bool {
|
||||
return p.AccessMode >= perm_model.AccessModeOwner
|
||||
}
|
||||
|
||||
// IsAdmin returns true if current user has admin or higher access of repository.
|
||||
func (p *Permission) IsAdmin() bool {
|
||||
return p.AccessMode >= perm_model.AccessModeAdmin
|
||||
}
|
||||
|
||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
|
||||
// It doesn't count the "public(anonymous/everyone) access mode".
|
||||
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
|
||||
func (p *Permission) HasAnyUnitAccess() bool {
|
||||
for _, v := range p.unitsMode {
|
||||
if v >= perm_model.AccessModeRead {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return p.AccessMode >= perm_model.AccessModeRead
|
||||
}
|
||||
|
||||
func (p *Permission) HasAnyUnitPublicAccess() bool {
|
||||
for _, v := range p.anonymousAccessMode {
|
||||
if v >= perm_model.AccessModeRead {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, v := range p.everyoneAccessMode {
|
||||
if v >= perm_model.AccessModeRead {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
|
||||
return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess()
|
||||
}
|
||||
|
||||
// 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 0
|
||||
}
|
||||
|
||||
// UnitAccessMode returns current user access mode to the specify unit of the repository
|
||||
// It also considers "public (anonymous/everyone) access mode"
|
||||
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
|
||||
// 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)
|
||||
}
|
||||
// if the units map does not contain the access mode, return the default access mode if the unit exists
|
||||
unitDefaultAccessMode := p.AccessMode
|
||||
unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType])
|
||||
unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType])
|
||||
hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
|
||||
return util.Iif(hasUnit, unitDefaultAccessMode, 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
|
||||
func (p *Permission) CanAccess(mode perm_model.AccessMode, unitType unit.Type) bool {
|
||||
return p.UnitAccessMode(unitType) >= mode
|
||||
}
|
||||
|
||||
// CanAccessAny returns true if user has mode access to any of the units of the repository
|
||||
func (p *Permission) CanAccessAny(mode perm_model.AccessMode, unitTypes ...unit.Type) bool {
|
||||
for _, u := range unitTypes {
|
||||
if p.CanAccess(mode, u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CanRead returns true if user could read to this unit
|
||||
func (p *Permission) CanRead(unitType unit.Type) bool {
|
||||
return p.CanAccess(perm_model.AccessModeRead, unitType)
|
||||
}
|
||||
|
||||
// CanReadAny returns true if user has read access to any of the units of the repository
|
||||
func (p *Permission) CanReadAny(unitTypes ...unit.Type) bool {
|
||||
return p.CanAccessAny(perm_model.AccessModeRead, unitTypes...)
|
||||
}
|
||||
|
||||
// CanReadIssuesOrPulls returns true if isPull is true and user could read pull requests and
|
||||
// returns true if isPull is false and user could read to issues
|
||||
func (p *Permission) CanReadIssuesOrPulls(isPull bool) bool {
|
||||
if isPull {
|
||||
return p.CanRead(unit.TypePullRequests)
|
||||
}
|
||||
return p.CanRead(unit.TypeIssues)
|
||||
}
|
||||
|
||||
// CanWrite returns true if user could write to this unit
|
||||
func (p *Permission) CanWrite(unitType unit.Type) bool {
|
||||
return p.CanAccess(perm_model.AccessModeWrite, unitType)
|
||||
}
|
||||
|
||||
// CanWriteIssuesOrPulls returns true if isPull is true and user could write to pull requests and
|
||||
// returns true if isPull is false and user could write to issues
|
||||
func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
|
||||
if isPull {
|
||||
return p.CanWrite(unit.TypePullRequests)
|
||||
}
|
||||
return p.CanWrite(unit.TypeIssues)
|
||||
}
|
||||
|
||||
func (p *Permission) ReadableUnitTypes() []unit.Type {
|
||||
types := make([]unit.Type, 0, len(p.units))
|
||||
for _, u := range p.units {
|
||||
if p.CanRead(u.Type) {
|
||||
types = append(types, u.Type)
|
||||
}
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
func (p *Permission) LogString() string {
|
||||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): ["
|
||||
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
|
||||
|
||||
for i, u := range p.units {
|
||||
config := ""
|
||||
if u.Config != nil {
|
||||
configBytes, err := u.Config.ToDB()
|
||||
config = string(configBytes)
|
||||
if err != nil {
|
||||
config = err.Error()
|
||||
}
|
||||
}
|
||||
format += "\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s"
|
||||
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
|
||||
}
|
||||
for key, value := range p.unitsMode {
|
||||
format += "\n\tunitsMode[%-v]: %-v"
|
||||
args = append(args, key.LogString(), value.LogString())
|
||||
}
|
||||
format += "\n\tanonymousAccessMode: %-v"
|
||||
args = append(args, p.anonymousAccessMode)
|
||||
format += "\n\teveryoneAccessMode: %-v"
|
||||
args = append(args, p.everyoneAccessMode)
|
||||
format += "\n\t]>"
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
|
||||
if setting.Repository.ForcePrivate {
|
||||
return
|
||||
}
|
||||
if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
|
||||
if *modeMap == nil {
|
||||
*modeMap = make(map[unit.Type]perm_model.AccessMode)
|
||||
}
|
||||
(*modeMap)[unitType] = accessMode
|
||||
}
|
||||
}
|
||||
|
||||
func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
|
||||
// apply public (anonymous) access permissions
|
||||
for _, u := range perm.units {
|
||||
applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode)
|
||||
}
|
||||
|
||||
if user == nil || user.ID <= 0 {
|
||||
// for anonymous access, it could be:
|
||||
// AccessMode is None or Read, units has repo units, unitModes is nil
|
||||
return
|
||||
}
|
||||
|
||||
// apply public (everyone) access permissions
|
||||
for _, u := range perm.units {
|
||||
applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode)
|
||||
}
|
||||
|
||||
if perm.unitsMode == nil {
|
||||
// if unitsMode is not set, then it means that the default p.AccessMode applies to all units
|
||||
return
|
||||
}
|
||||
|
||||
// remove no permission units
|
||||
origPermUnits := perm.units
|
||||
perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units))
|
||||
for _, u := range origPermUnits {
|
||||
shouldKeep := false
|
||||
for t := range perm.unitsMode {
|
||||
if shouldKeep = u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
for t := range perm.anonymousAccessMode {
|
||||
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
for t := range perm.everyoneAccessMode {
|
||||
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldKeep {
|
||||
perm.units = append(perm.units, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
}
|
||||
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.
|
||||
if user == nil && repo.IsPrivate {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
var isCollaborator bool
|
||||
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 {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
// Prevent strangers from checking out public repo of private organization/users
|
||||
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
||||
// TODO: rename it to "IsOwnerVisibleToDoer"
|
||||
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// anonymous visit public repo
|
||||
if user == nil {
|
||||
perm.AccessMode = perm_model.AccessModeRead
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// Admin or the owner has super access to the repository
|
||||
if user.IsAdmin || user.ID == repo.OwnerID {
|
||||
perm.AccessMode = perm_model.AccessModeOwner
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// plain user TODO: this check should be replaced, only need to check collaborator access mode
|
||||
perm.AccessMode, err = accessLevel(ctx, user, repo)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
if !repo.Owner.IsOrganization() {
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// now: the owner is visible to doer, if the repo is public, then the min access mode is read
|
||||
minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
|
||||
perm.AccessMode = max(perm.AccessMode, minAccessMode)
|
||||
|
||||
// get units mode from teams
|
||||
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
if len(teams) == 0 {
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// if user in an owner team
|
||||
for _, team := range teams {
|
||||
if team.HasAdminAccess() {
|
||||
perm.AccessMode = perm_model.AccessModeOwner
|
||||
perm.unitsMode = nil
|
||||
return perm, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range repo.Units {
|
||||
for _, team := range teams {
|
||||
unitAccessMode := minAccessMode
|
||||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
|
||||
unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
|
||||
}
|
||||
perm.unitsMode[u.Type] = unitAccessMode
|
||||
}
|
||||
}
|
||||
|
||||
return perm, err
|
||||
}
|
||||
|
||||
// IsUserRealRepoAdmin check if this user is real repo admin
|
||||
func IsUserRealRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
|
||||
if repo.OwnerID == user.ID {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
accessMode, err := accessLevel(ctx, user, repo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return accessMode >= perm_model.AccessModeAdmin, nil
|
||||
}
|
||||
|
||||
// IsUserRepoAdmin return true if user has admin right of a repo
|
||||
func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
|
||||
if user == nil || repo == nil {
|
||||
return false, nil
|
||||
}
|
||||
if user.IsAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
mode, err := accessLevel(ctx, user, repo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if mode >= perm_model.AccessModeAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if team.HasAdminAccess() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
|
||||
// user does not have access.
|
||||
func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint:revive // export stutter
|
||||
return AccessLevelUnit(ctx, user, repo, unit.TypeCode)
|
||||
}
|
||||
|
||||
// AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the
|
||||
// user does not have access.
|
||||
func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return perm_model.AccessModeNone, err
|
||||
}
|
||||
return perm.UnitAccessMode(unitType), nil
|
||||
}
|
||||
|
||||
// HasAccessUnit returns true if user has testMode to the unit of the repository
|
||||
func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type, testMode perm_model.AccessMode) (bool, error) {
|
||||
mode, err := AccessLevelUnit(ctx, user, repo, unitType)
|
||||
return testMode <= mode, err
|
||||
}
|
||||
|
||||
// CanBeAssigned return true if user can be assigned to issue or pull requests in repo
|
||||
// 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)
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) ||
|
||||
perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil
|
||||
}
|
||||
|
||||
// HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess"
|
||||
func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) {
|
||||
var user *user_model.User
|
||||
var err error
|
||||
if userID > 0 {
|
||||
user, err = user_model.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return perm.HasAnyUnitAccess(), nil
|
||||
}
|
||||
|
||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
||||
if len(accesses) > 0 {
|
||||
userIDs := make([]int64, len(accesses))
|
||||
for i := 0; i < len(accesses); i++ {
|
||||
userIDs[i] = accesses[i].UserID
|
||||
}
|
||||
|
||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
users = append(users, repo.Owner)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
||||
}
|
||||
|
||||
// GetRepoWriters returns all users that have write access to the repository.
|
||||
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
|
||||
}
|
||||
|
||||
// IsRepoReader returns true if user has explicit read access or higher to the repository.
|
||||
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
|
||||
if repo.OwnerID == userID {
|
||||
return true, nil
|
||||
}
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
|
||||
}
|
||||
|
||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||
func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
|
||||
if user != nil && user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission: %w", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return perm.CanRead(unitType)
|
||||
}
|
||||
|
||||
func PermissionNoAccess() Permission {
|
||||
return Permission{AccessMode: perm_model.AccessModeNone}
|
||||
}
|
200
models/perm/access/repo_permission_test.go
Normal file
200
models/perm/access/repo_permission_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHasAnyUnitAccess(t *testing.T) {
|
||||
perm := Permission{}
|
||||
assert.False(t, perm.HasAnyUnitAccess())
|
||||
|
||||
perm = Permission{
|
||||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
|
||||
}
|
||||
assert.False(t, perm.HasAnyUnitAccess())
|
||||
assert.False(t, perm.HasAnyUnitAccessOrPublicAccess())
|
||||
|
||||
perm = Permission{
|
||||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
|
||||
everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
|
||||
}
|
||||
assert.False(t, perm.HasAnyUnitAccess())
|
||||
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
|
||||
|
||||
perm = Permission{
|
||||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
|
||||
anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
|
||||
}
|
||||
assert.False(t, perm.HasAnyUnitAccess())
|
||||
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
|
||||
|
||||
perm = Permission{
|
||||
AccessMode: perm_model.AccessModeRead,
|
||||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
|
||||
}
|
||||
assert.True(t, perm.HasAnyUnitAccess())
|
||||
|
||||
perm = Permission{
|
||||
unitsMode: map[unit.Type]perm_model.AccessMode{unit.TypeWiki: perm_model.AccessModeRead},
|
||||
}
|
||||
assert.True(t, perm.HasAnyUnitAccess())
|
||||
}
|
||||
|
||||
func TestApplyPublicAccessRepoPermission(t *testing.T) {
|
||||
perm := Permission{
|
||||
AccessMode: perm_model.AccessModeNone,
|
||||
units: []*repo_model.RepoUnit{
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(nil, &perm)
|
||||
assert.False(t, perm.CanRead(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
AccessMode: perm_model.AccessModeNone,
|
||||
units: []*repo_model.RepoUnit{
|
||||
{Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(nil, &perm)
|
||||
assert.True(t, perm.CanRead(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
AccessMode: perm_model.AccessModeNone,
|
||||
units: []*repo_model.RepoUnit{
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 0}, &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},
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(&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},
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
|
||||
// it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units
|
||||
assert.True(t, perm.CanWrite(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
units: []*repo_model.RepoUnit{
|
||||
{Type: unit.TypeCode}, // will be removed
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
unitsMode: map[unit.Type]perm_model.AccessMode{
|
||||
unit.TypeWiki: perm_model.AccessModeWrite,
|
||||
},
|
||||
}
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
|
||||
assert.True(t, perm.CanWrite(unit.TypeWiki))
|
||||
assert.Len(t, perm.units, 1)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
|
||||
require.NoError(t, repo32.LoadOwner(ctx))
|
||||
require.True(t, repo32.Owner.IsOrganization())
|
||||
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
|
||||
org := repo32.Owner
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||
require.NoError(t, db.Insert(ctx, team))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
|
||||
})
|
||||
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
})
|
||||
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
})
|
||||
}
|
65
models/perm/access_mode.go
Normal file
65
models/perm/access_mode.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package perm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// AccessMode specifies the users access mode
|
||||
type AccessMode int
|
||||
|
||||
const (
|
||||
AccessModeNone AccessMode = iota // 0: no access
|
||||
|
||||
AccessModeRead // 1: read access
|
||||
AccessModeWrite // 2: write access
|
||||
AccessModeAdmin // 3: admin access
|
||||
AccessModeOwner // 4: owner access
|
||||
)
|
||||
|
||||
// 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"
|
||||
case AccessModeWrite:
|
||||
return "write"
|
||||
case AccessModeAdmin:
|
||||
return "admin"
|
||||
case AccessModeOwner:
|
||||
return "owner"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
func (mode AccessMode) LogString() string {
|
||||
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.ToString())
|
||||
}
|
||||
|
||||
// ParseAccessMode returns corresponding access mode to given permission string.
|
||||
func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode {
|
||||
m := AccessModeNone
|
||||
switch permission {
|
||||
case "read":
|
||||
m = AccessModeRead
|
||||
case "write":
|
||||
m = AccessModeWrite
|
||||
case "admin":
|
||||
m = AccessModeAdmin
|
||||
default:
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ErrInvalidAccessMode is returned when an invalid access mode is used
|
||||
var ErrInvalidAccessMode = util.NewInvalidArgumentErrorf("Invalid access mode")
|
22
models/perm/access_mode_test.go
Normal file
22
models/perm/access_mode_test.go
Normal file
@@ -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, AccessModeOwner, AccessMode(4))
|
||||
assert.Equal(t, "owner", AccessModeOwner.ToString())
|
||||
assert.Equal(t, AccessModeNone, ParseAccessMode("owner"))
|
||||
assert.Equal(t, AccessModeNone, ParseAccessMode("invalid"))
|
||||
}
|
Reference in New Issue
Block a user