summaryrefslogtreecommitdiffstats
path: root/models/perm/access
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /models/perm/access
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--models/perm/access/access.go250
-rw-r--r--models/perm/access/access_test.go127
-rw-r--r--models/perm/access/main_test.go20
-rw-r--r--models/perm/access/repo_permission.go450
-rw-r--r--models/perm/access_mode.go57
5 files changed, 904 insertions, 0 deletions
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
new file mode 100644
index 0000000..3e2568b
--- /dev/null
+++ b/models/perm/access/access.go
@@ -0,0 +1,250 @@
+// 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 {
+ 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(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, repoID, db.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("getCollaborations: %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)
+}
diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go
new file mode 100644
index 0000000..556f513
--- /dev/null
+++ b/models/perm/access/access_test.go
@@ -0,0 +1,127 @@
+// 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"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccessLevel(t *testing.T) {
+ require.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)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeOwner, level)
+
+ level, err = access_model.AccessLevel(db.DefaultContext, user2, repo3)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeOwner, level)
+
+ level, err = access_model.AccessLevel(db.DefaultContext, user5, repo1)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeRead, level)
+
+ level, err = access_model.AccessLevel(db.DefaultContext, user5, repo3)
+ require.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)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeNone, level)
+
+ // ... unless he's a collaborator
+ level, err = access_model.AccessLevel(db.DefaultContext, user29, repo4)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeWrite, level)
+
+ // ... or a team member
+ level, err = access_model.AccessLevel(db.DefaultContext, user29, repo24)
+ require.NoError(t, err)
+ assert.Equal(t, perm_model.AccessModeRead, level)
+}
+
+func TestHasAccess(t *testing.T) {
+ require.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.HasAccess(db.DefaultContext, user1.ID, repo1)
+ require.NoError(t, err)
+ assert.True(t, has)
+
+ _, err = access_model.HasAccess(db.DefaultContext, user1.ID, repo2)
+ require.NoError(t, err)
+
+ _, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo1)
+ require.NoError(t, err)
+
+ _, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo2)
+ require.NoError(t, err)
+}
+
+func TestRepository_RecalculateAccesses(t *testing.T) {
+ // test with organization repo
+ require.NoError(t, unittest.PrepareTestDatabase())
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ require.NoError(t, repo1.LoadOwner(db.DefaultContext))
+
+ _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3})
+ require.NoError(t, err)
+ require.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1))
+
+ access := &access_model.Access{UserID: 2, RepoID: 3}
+ has, err := db.GetEngine(db.DefaultContext).Get(access)
+ require.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
+ require.NoError(t, unittest.PrepareTestDatabase())
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
+ require.NoError(t, repo1.LoadOwner(db.DefaultContext))
+
+ _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4})
+ require.NoError(t, err)
+ require.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1))
+
+ has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 4, RepoID: 4})
+ require.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 0000000..0a350dc
--- /dev/null
+++ b/models/perm/access/main_test.go
@@ -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)
+}
diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
new file mode 100644
index 0000000..7e39627
--- /dev/null
+++ b/models/perm/access/repo_permission.go
@@ -0,0 +1,450 @@
+// Copyright 2018 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"
+ 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
+}
+
+// IsGloballyWriteable returns true if the unit is writeable by all users of the instance.
+func (p *Permission) IsGloballyWriteable(unitType unit.Type) bool {
+ for _, u := range p.Units {
+ if u.Type == unitType {
+ return u.DefaultPermissions == repo_model.UnitAccessModeWrite
+ }
+ }
+ return false
+}
+
+// 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)
+}
+
+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)}
+
+ 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: %s Config: %s"
+ args = append(args, i, unit.ID, unit.RepoID, unit.Type.LogString(), config)
+ }
+ for key, value := range p.UnitsMode {
+ format += "\nUnitMode[%-v]: %-v"
+ args = append(args, key.LogString(), value.LogString())
+ }
+ format += " ]>"
+ 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
+ }
+ 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 perm, nil
+ }
+
+ 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 {
+ return perm, err
+ }
+
+ // 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) && !isCollaborator {
+ perm.AccessMode = perm_model.AccessModeNone
+ 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
+ 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
+ perm.AccessMode, err = accessLevel(ctx, user, repo)
+ if err != nil {
+ return perm, err
+ }
+
+ if err := repo.LoadOwner(ctx); err != nil {
+ return perm, err
+ }
+
+ if !repo.Owner.IsOrganization() {
+ // for a public repo, different repo units may have different default
+ // permissions for non-restricted users.
+ if !repo.IsPrivate && !user.IsRestricted && len(repo.Units) > 0 {
+ perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode)
+ for _, u := range repo.Units {
+ if _, ok := perm.UnitsMode[u.Type]; !ok {
+ perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm.AccessMode)
+ }
+ }
+ }
+
+ 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
+ }
+ }
+
+ // get units mode from teams
+ teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
+ if err != nil {
+ return perm, err
+ }
+
+ // 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 perm, nil
+ }
+ }
+
+ for _, u := range repo.Units {
+ var found bool
+ for _, team := range teams {
+ teamMode := team.UnitAccessMode(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 should
+ // have the same permission on non-team defined units as the default
+ // permissions for the repo unit.
+ if !found && !repo.IsPrivate && !user.IsRestricted {
+ if _, ok := perm.UnitsMode[u.Type]; !ok {
+ perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(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 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.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(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint
+ 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
+ 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
+}
+
+// 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.GetUserByID(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.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)
+}
diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go
new file mode 100644
index 0000000..a37bc1f
--- /dev/null
+++ b/models/perm/access_mode.go
@@ -0,0 +1,57 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package perm
+
+import (
+ "fmt"
+)
+
+// 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
+)
+
+func (mode AccessMode) String() 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.String())
+}
+
+// ParseAccessMode returns corresponding access mode to given permission string.
+func ParseAccessMode(permission string) AccessMode {
+ switch permission {
+ case "read":
+ return AccessModeRead
+ case "write":
+ return AccessModeWrite
+ case "admin":
+ return AccessModeAdmin
+ default:
+ return AccessModeNone
+ }
+}