diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2022-05-11 18:09:36 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-11 12:09:36 +0200 |
commit | cbd45471b1100bffcd2f18719b56a5da5468756b (patch) | |
tree | 11fcb818fd4e9d7b6bdb97d1b5bfbe2ed3df5747 /models/perm/access | |
parent | 8e8e936adacea873013b89d534f60207e67f2b05 (diff) | |
download | gitea-cbd45471b1100bffcd2f18719b56a5da5468756b.tar.gz gitea-cbd45471b1100bffcd2f18719b56a5da5468756b.zip |
Move access and repo permission to models/perm/access (#19350)
* Move access and repo permission to models/perm/access
* Remove unnecessary code
Diffstat (limited to 'models/perm/access')
-rw-r--r-- | models/perm/access/access.go | 244 | ||||
-rw-r--r-- | models/perm/access/access_test.go | 126 | ||||
-rw-r--r-- | models/perm/access/main_test.go | 32 | ||||
-rw-r--r-- | models/perm/access/repo_permission.go | 440 |
4 files changed, 842 insertions, 0 deletions
diff --git a/models/perm/access/access.go b/models/perm/access/access.go new file mode 100644 index 0000000000..75a3a93a3b --- /dev/null +++ b/models/perm/access/access.go @@ -0,0 +1,244 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +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" +) + +// 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(e db.Engine, 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 := &Access{UserID: userID, RepoID: repo.ID} + if has, err := e.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil +} + +func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { + max := perm.AccessModeNone + for _, mode := range modes { + if mode > max { + max = mode + } + } + return max +} + +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(e db.Engine, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) { + minMode := perm.AccessModeRead + if !repo.IsPrivate { + 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 = e.Delete(&Access{RepoID: repo.ID}); err != nil { + return fmt.Errorf("delete old accesses: %v", err) + } + if len(newAccesses) == 0 { + return nil + } + + if _, err = e.Insert(newAccesses); err != nil { + return fmt.Errorf("insert new accesses: %v", 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, repoID, db.ListOptions{}) + if err != nil { + return fmt.Errorf("getCollaborations: %v", 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.GetOwner(ctx); err != nil { + return err + } else if !repo.Owner.IsOrganization() { + return fmt.Errorf("owner is not an organization: %d", repo.OwnerID) + } + + e := db.GetEngine(ctx) + + if err = refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil { + return fmt.Errorf("refreshCollaboratorAccesses: %v", 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.GetMembersCtx(ctx); err != nil { + return fmt.Errorf("getMembers '%d': %v", t.ID, err) + } + for _, m := range t.Members { + updateUserAccess(accessMap, m, t.AccessMode) + } + } + + return refreshAccesses(e, 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.GetOwner(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: %v", 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: %v", 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) + } + + e := db.GetEngine(ctx) + accessMap := make(map[int64]*userAccess, 20) + if err := refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil { + return fmt.Errorf("refreshCollaboratorAccesses: %v", err) + } + return refreshAccesses(e, repo, accessMap) +} diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go new file mode 100644 index 0000000000..a9ae9a30f4 --- /dev/null +++ b/models/perm/access/access_test.go @@ -0,0 +1,126 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package access + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + 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}).(*user_model.User) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) + user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}).(*user_model.User) + // A public repository owned by User 2 + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.False(t, repo1.IsPrivate) + // A private repository owned by Org 3 + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + assert.True(t, repo3.IsPrivate) + + // Another public repository + repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}).(*repo_model.Repository) + assert.False(t, repo4.IsPrivate) + // org. owned private repo + repo24 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24}).(*repo_model.Repository) + + level, err := AccessLevel(user2, repo1) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeOwner, level) + + level, err = AccessLevel(user2, repo3) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeOwner, level) + + level, err = AccessLevel(user5, repo1) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeRead, level) + + level, err = AccessLevel(user5, repo3) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, level) + + // restricted user has no access to a public repo + level, err = AccessLevel(user29, repo1) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, level) + + // ... unless he's a collaborator + level, err = AccessLevel(user29, repo4) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeWrite, level) + + // ... or a team member + level, err = AccessLevel(user29, repo24) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeRead, level) +} + +func TestHasAccess(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) + // A public repository owned by User 2 + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.False(t, repo1.IsPrivate) + // A private repository owned by Org 3 + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + assert.True(t, repo2.IsPrivate) + + has, err := HasAccess(db.DefaultContext, user1.ID, repo1) + assert.NoError(t, err) + assert.True(t, has) + + _, err = HasAccess(db.DefaultContext, user1.ID, repo2) + assert.NoError(t, err) + + _, err = HasAccess(db.DefaultContext, user2.ID, repo1) + assert.NoError(t, err) + + _, err = HasAccess(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}).(*repo_model.Repository) + assert.NoError(t, repo1.GetOwner(db.DefaultContext)) + + _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3}) + assert.NoError(t, err) + assert.NoError(t, RecalculateAccesses(db.DefaultContext, repo1)) + + access := &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.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}).(*repo_model.Repository) + assert.NoError(t, repo1.GetOwner(db.DefaultContext)) + + _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4}) + assert.NoError(t, err) + assert.NoError(t, RecalculateAccesses(db.DefaultContext, repo1)) + + has, err := db.GetEngine(db.DefaultContext).Get(&Access{UserID: 4, RepoID: 4}) + assert.NoError(t, err) + assert.False(t, has) +} diff --git a/models/perm/access/main_test.go b/models/perm/access/main_test.go new file mode 100644 index 0000000000..153ac2540d --- /dev/null +++ b/models/perm/access/main_test.go @@ -0,0 +1,32 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package access + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/repo" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", "..", ".."), + FixtureFiles: []string{ + "access.yml", + "user.yml", + "repository.yml", + "collaboration.yml", + "org_user.yml", + "repo_unit.yml", + "team_user.yml", + "team_repo.yml", + "team.yml", + "team_unit.yml", + }, + }) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go new file mode 100644 index 0000000000..090c78ff2c --- /dev/null +++ b/models/perm/access/repo_permission.go @@ -0,0 +1,440 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package access + +import ( + "context" + "fmt" + + "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" +) + +// 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 +} + +// 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 +} + +// HasAccess returns true if the current user has 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 +} + +// UnitAccessMode returns current user accessmode 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 + } + } + return perm_model.AccessModeNone + } + return p.UnitsMode[unitType] +} + +// 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) +} + +// ColorFormat writes a colored string for these Permissions +func (p *Permission) ColorFormat(s fmt.State) { + noColor := log.ColorBytes(log.Reset) + + format := "perm_model.AccessMode: %-v, %d Units, %d UnitsMode(s): [ " + args := []interface{}{ + p.AccessMode, + log.NewColoredValueBytes(len(p.Units), &noColor), + log.NewColoredValueBytes(len(p.UnitsMode), &noColor), + } + if s.Flag('+') { + for i, unit := range p.Units { + config := "" + if unit.Config != nil { + configBytes, err := unit.Config.ToDB() + config = string(configBytes) + if err != nil { + config = err.Error() + } + } + format += "\nUnits[%d]: ID: %d RepoID: %d Type: %-v Config: %s" + args = append(args, + log.NewColoredValueBytes(i, &noColor), + log.NewColoredIDValue(unit.ID), + log.NewColoredIDValue(unit.RepoID), + unit.Type, + config) + } + for key, value := range p.UnitsMode { + format += "\nUnitMode[%-v]: %-v" + args = append(args, + key, + value) + } + } else { + format += "..." + } + format += " ]" + log.ColorFprintf(s, format, args...) +} + +// 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) { + if log.IsTrace() { + defer func() { + if user == nil { + log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v", + repo, + perm) + return + } + log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v", + user, + repo, + perm) + }() + } + + // anonymous user visit private repo. + // TODO: anonymous user visit public unit of private repo??? + if user == nil && repo.IsPrivate { + perm.AccessMode = perm_model.AccessModeNone + return + } + + e := db.GetEngine(ctx) + + var is bool + if user != nil { + is, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) + if err != nil { + return perm, err + } + } + + if err = repo.GetOwner(ctx); err != nil { + return + } + + // Prevent strangers from checking out public repo of private organization/users + // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !is { + perm.AccessMode = perm_model.AccessModeNone + return + } + + if err = repo.LoadUnits(ctx); err != nil { + return + } + + perm.Units = repo.Units + + // anonymous visit public repo + if user == nil { + perm.AccessMode = perm_model.AccessModeRead + return + } + + // Admin or the owner has super access to the repository + if user.IsAdmin || user.ID == repo.OwnerID { + perm.AccessMode = perm_model.AccessModeOwner + return + } + + // plain user + perm.AccessMode, err = accessLevel(e, user, repo) + if err != nil { + return + } + + if err = repo.GetOwner(ctx); err != nil { + return + } + if !repo.Owner.IsOrganization() { + return + } + + perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + + // Collaborators on organization + if is { + for _, u := range repo.Units { + perm.UnitsMode[u.Type] = perm.AccessMode + } + } + + // get units mode from teams + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) + if err != nil { + return + } + + // if user in an owner team + for _, team := range teams { + if team.AccessMode >= perm_model.AccessModeAdmin { + perm.AccessMode = perm_model.AccessModeOwner + perm.UnitsMode = nil + return + } + } + + for _, u := range repo.Units { + var found bool + for _, team := range teams { + teamMode := team.UnitAccessModeCtx(ctx, u.Type) + if teamMode > perm_model.AccessModeNone { + m := perm.UnitsMode[u.Type] + if m < teamMode { + 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 + } + } + } + + // remove no permission units + 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) + } + } + } + + return +} + +// IsUserRealRepoAdmin check if this user is real repo admin +func IsUserRealRepoAdmin(repo *repo_model.Repository, user *user_model.User) (bool, error) { + if repo.OwnerID == user.ID { + return true, nil + } + + if err := repo.GetOwner(db.DefaultContext); err != nil { + return false, err + } + + accessMode, err := accessLevel(db.GetEngine(db.DefaultContext), 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(repo *repo_model.Repository, user *user_model.User) (bool, error) { + return IsUserRepoAdminCtx(db.DefaultContext, repo, user) +} + +// IsUserRepoAdminCtx return true if user has admin right of a repo +func IsUserRepoAdminCtx(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 + } + + e := db.GetEngine(ctx) + mode, err := accessLevel(e, 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.AccessMode >= perm_model.AccessModeAdmin { + 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(user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint + return AccessLevelUnit(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(user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint + return accessLevelUnit(db.DefaultContext, user, repo, unitType) +} + +func accessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { + 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. +// FIXME: user could send PullRequest also could be assigned??? +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.TypeCode, unit.TypeIssues, unit.TypePullRequests), nil +} + +// HasAccess returns true if user has access to repo +func HasAccess(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.GetUserByIDEngine(db.GetEngine(ctx), userID) + if err != nil { + return false, err + } + } + perm, err := GetUserRepoPermission(ctx, repo, user) + if err != nil { + return false, err + } + return perm.HasAccess(), 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.GetOwner(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(repo *repo_model.Repository) (_ []*user_model.User, err error) { + return getUsersWithAccessMode(db.DefaultContext, repo, perm_model.AccessModeRead) +} + +// GetRepoWriters returns all users that have write access to the repository. +func GetRepoWriters(repo *repo_model.Repository) (_ []*user_model.User, err error) { + return getUsersWithAccessMode(db.DefaultContext, 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{}) +} |