code.gitea.io/gitea@v1.22.3/models/perm/access/repo_permission.go (about)

     1  // Copyright 2018 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package access
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"slices"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	"code.gitea.io/gitea/models/organization"
    13  	perm_model "code.gitea.io/gitea/models/perm"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	"code.gitea.io/gitea/models/unit"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/util"
    19  )
    20  
    21  // Permission contains all the permissions related variables to a repository for a user
    22  type Permission struct {
    23  	AccessMode perm_model.AccessMode
    24  
    25  	units     []*repo_model.RepoUnit
    26  	unitsMode map[unit.Type]perm_model.AccessMode
    27  
    28  	everyoneAccessMode map[unit.Type]perm_model.AccessMode
    29  }
    30  
    31  // IsOwner returns true if current user is the owner of repository.
    32  func (p *Permission) IsOwner() bool {
    33  	return p.AccessMode >= perm_model.AccessModeOwner
    34  }
    35  
    36  // IsAdmin returns true if current user has admin or higher access of repository.
    37  func (p *Permission) IsAdmin() bool {
    38  	return p.AccessMode >= perm_model.AccessModeAdmin
    39  }
    40  
    41  // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
    42  // It doesn't count the "everyone access mode".
    43  func (p *Permission) HasAnyUnitAccess() bool {
    44  	for _, v := range p.unitsMode {
    45  		if v >= perm_model.AccessModeRead {
    46  			return true
    47  		}
    48  	}
    49  	return p.AccessMode >= perm_model.AccessModeRead
    50  }
    51  
    52  func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool {
    53  	for _, v := range p.everyoneAccessMode {
    54  		if v >= perm_model.AccessModeRead {
    55  			return true
    56  		}
    57  	}
    58  	return p.HasAnyUnitAccess()
    59  }
    60  
    61  // HasUnits returns true if the permission contains attached units
    62  func (p *Permission) HasUnits() bool {
    63  	return len(p.units) > 0
    64  }
    65  
    66  // GetFirstUnitRepoID returns the repo ID of the first unit, it is a fragile design and should NOT be used anymore
    67  // deprecated
    68  func (p *Permission) GetFirstUnitRepoID() int64 {
    69  	if len(p.units) > 0 {
    70  		return p.units[0].RepoID
    71  	}
    72  	return 0
    73  }
    74  
    75  // UnitAccessMode returns current user access mode to the specify unit of the repository
    76  // It also considers "everyone access mode"
    77  func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
    78  	// if the units map contains the access mode, use it, but admin/owner mode could override it
    79  	if m, ok := p.unitsMode[unitType]; ok {
    80  		return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m)
    81  	}
    82  	// if the units map does not contain the access mode, return the default access mode if the unit exists
    83  	unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType])
    84  	hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
    85  	return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone)
    86  }
    87  
    88  func (p *Permission) SetUnitsWithDefaultAccessMode(units []*repo_model.RepoUnit, mode perm_model.AccessMode) {
    89  	p.units = units
    90  	p.unitsMode = make(map[unit.Type]perm_model.AccessMode)
    91  	for _, u := range p.units {
    92  		p.unitsMode[u.Type] = mode
    93  	}
    94  }
    95  
    96  // CanAccess returns true if user has mode access to the unit of the repository
    97  func (p *Permission) CanAccess(mode perm_model.AccessMode, unitType unit.Type) bool {
    98  	return p.UnitAccessMode(unitType) >= mode
    99  }
   100  
   101  // CanAccessAny returns true if user has mode access to any of the units of the repository
   102  func (p *Permission) CanAccessAny(mode perm_model.AccessMode, unitTypes ...unit.Type) bool {
   103  	for _, u := range unitTypes {
   104  		if p.CanAccess(mode, u) {
   105  			return true
   106  		}
   107  	}
   108  	return false
   109  }
   110  
   111  // CanRead returns true if user could read to this unit
   112  func (p *Permission) CanRead(unitType unit.Type) bool {
   113  	return p.CanAccess(perm_model.AccessModeRead, unitType)
   114  }
   115  
   116  // CanReadAny returns true if user has read access to any of the units of the repository
   117  func (p *Permission) CanReadAny(unitTypes ...unit.Type) bool {
   118  	return p.CanAccessAny(perm_model.AccessModeRead, unitTypes...)
   119  }
   120  
   121  // CanReadIssuesOrPulls returns true if isPull is true and user could read pull requests and
   122  // returns true if isPull is false and user could read to issues
   123  func (p *Permission) CanReadIssuesOrPulls(isPull bool) bool {
   124  	if isPull {
   125  		return p.CanRead(unit.TypePullRequests)
   126  	}
   127  	return p.CanRead(unit.TypeIssues)
   128  }
   129  
   130  // CanWrite returns true if user could write to this unit
   131  func (p *Permission) CanWrite(unitType unit.Type) bool {
   132  	return p.CanAccess(perm_model.AccessModeWrite, unitType)
   133  }
   134  
   135  // CanWriteIssuesOrPulls returns true if isPull is true and user could write to pull requests and
   136  // returns true if isPull is false and user could write to issues
   137  func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
   138  	if isPull {
   139  		return p.CanWrite(unit.TypePullRequests)
   140  	}
   141  	return p.CanWrite(unit.TypeIssues)
   142  }
   143  
   144  func (p *Permission) ReadableUnitTypes() []unit.Type {
   145  	types := make([]unit.Type, 0, len(p.units))
   146  	for _, u := range p.units {
   147  		if p.CanRead(u.Type) {
   148  			types = append(types, u.Type)
   149  		}
   150  	}
   151  	return types
   152  }
   153  
   154  func (p *Permission) LogString() string {
   155  	format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ "
   156  	args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
   157  
   158  	for i, u := range p.units {
   159  		config := ""
   160  		if u.Config != nil {
   161  			configBytes, err := u.Config.ToDB()
   162  			config = string(configBytes)
   163  			if err != nil {
   164  				config = err.Error()
   165  			}
   166  		}
   167  		format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s"
   168  		args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
   169  	}
   170  	for key, value := range p.unitsMode {
   171  		format += "\nUnitMode[%-v]: %-v"
   172  		args = append(args, key.LogString(), value.LogString())
   173  	}
   174  	format += " ]>"
   175  	return fmt.Sprintf(format, args...)
   176  }
   177  
   178  func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
   179  	if user == nil || user.ID <= 0 {
   180  		return
   181  	}
   182  	for _, u := range perm.units {
   183  		if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
   184  			if perm.everyoneAccessMode == nil {
   185  				perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode)
   186  			}
   187  			perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
   188  		}
   189  	}
   190  }
   191  
   192  // GetUserRepoPermission returns the user permissions to the repository
   193  func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
   194  	defer func() {
   195  		if err == nil {
   196  			applyEveryoneRepoPermission(user, &perm)
   197  		}
   198  		if log.IsTrace() {
   199  			log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
   200  		}
   201  	}()
   202  
   203  	if err = repo.LoadUnits(ctx); err != nil {
   204  		return perm, err
   205  	}
   206  	perm.units = repo.Units
   207  
   208  	// anonymous user visit private repo.
   209  	// TODO: anonymous user visit public unit of private repo???
   210  	if user == nil && repo.IsPrivate {
   211  		perm.AccessMode = perm_model.AccessModeNone
   212  		return perm, nil
   213  	}
   214  
   215  	var isCollaborator bool
   216  	if user != nil {
   217  		isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
   218  		if err != nil {
   219  			return perm, err
   220  		}
   221  	}
   222  
   223  	if err = repo.LoadOwner(ctx); err != nil {
   224  		return perm, err
   225  	}
   226  
   227  	// Prevent strangers from checking out public repo of private organization/users
   228  	// 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
   229  	if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
   230  		perm.AccessMode = perm_model.AccessModeNone
   231  		return perm, nil
   232  	}
   233  
   234  	// anonymous visit public repo
   235  	if user == nil {
   236  		perm.AccessMode = perm_model.AccessModeRead
   237  		return perm, nil
   238  	}
   239  
   240  	// Admin or the owner has super access to the repository
   241  	if user.IsAdmin || user.ID == repo.OwnerID {
   242  		perm.AccessMode = perm_model.AccessModeOwner
   243  		return perm, nil
   244  	}
   245  
   246  	// plain user
   247  	perm.AccessMode, err = accessLevel(ctx, user, repo)
   248  	if err != nil {
   249  		return perm, err
   250  	}
   251  
   252  	if !repo.Owner.IsOrganization() {
   253  		return perm, nil
   254  	}
   255  
   256  	perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
   257  
   258  	// Collaborators on organization
   259  	if isCollaborator {
   260  		for _, u := range repo.Units {
   261  			perm.unitsMode[u.Type] = perm.AccessMode
   262  		}
   263  	}
   264  
   265  	// get units mode from teams
   266  	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
   267  	if err != nil {
   268  		return perm, err
   269  	}
   270  
   271  	// if user in an owner team
   272  	for _, team := range teams {
   273  		if team.AccessMode >= perm_model.AccessModeAdmin {
   274  			perm.AccessMode = perm_model.AccessModeOwner
   275  			perm.unitsMode = nil
   276  			return perm, nil
   277  		}
   278  	}
   279  
   280  	for _, u := range repo.Units {
   281  		var found bool
   282  		for _, team := range teams {
   283  			if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
   284  				perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
   285  				found = true
   286  			}
   287  		}
   288  
   289  		// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
   290  		if !found && !repo.IsPrivate && !user.IsRestricted {
   291  			if _, ok := perm.unitsMode[u.Type]; !ok {
   292  				perm.unitsMode[u.Type] = perm_model.AccessModeRead
   293  			}
   294  		}
   295  	}
   296  
   297  	// remove no permission units
   298  	perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
   299  	for t := range perm.unitsMode {
   300  		for _, u := range repo.Units {
   301  			if u.Type == t {
   302  				perm.units = append(perm.units, u)
   303  			}
   304  		}
   305  	}
   306  
   307  	return perm, err
   308  }
   309  
   310  // IsUserRealRepoAdmin check if this user is real repo admin
   311  func IsUserRealRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
   312  	if repo.OwnerID == user.ID {
   313  		return true, nil
   314  	}
   315  
   316  	if err := repo.LoadOwner(ctx); err != nil {
   317  		return false, err
   318  	}
   319  
   320  	accessMode, err := accessLevel(ctx, user, repo)
   321  	if err != nil {
   322  		return false, err
   323  	}
   324  
   325  	return accessMode >= perm_model.AccessModeAdmin, nil
   326  }
   327  
   328  // IsUserRepoAdmin return true if user has admin right of a repo
   329  func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
   330  	if user == nil || repo == nil {
   331  		return false, nil
   332  	}
   333  	if user.IsAdmin {
   334  		return true, nil
   335  	}
   336  
   337  	mode, err := accessLevel(ctx, user, repo)
   338  	if err != nil {
   339  		return false, err
   340  	}
   341  	if mode >= perm_model.AccessModeAdmin {
   342  		return true, nil
   343  	}
   344  
   345  	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
   346  	if err != nil {
   347  		return false, err
   348  	}
   349  
   350  	for _, team := range teams {
   351  		if team.AccessMode >= perm_model.AccessModeAdmin {
   352  			return true, nil
   353  		}
   354  	}
   355  	return false, nil
   356  }
   357  
   358  // AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
   359  // user does not have access.
   360  func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint
   361  	return AccessLevelUnit(ctx, user, repo, unit.TypeCode)
   362  }
   363  
   364  // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the
   365  // user does not have access.
   366  func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint
   367  	perm, err := GetUserRepoPermission(ctx, repo, user)
   368  	if err != nil {
   369  		return perm_model.AccessModeNone, err
   370  	}
   371  	return perm.UnitAccessMode(unitType), nil
   372  }
   373  
   374  // HasAccessUnit returns true if user has testMode to the unit of the repository
   375  func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type, testMode perm_model.AccessMode) (bool, error) {
   376  	mode, err := AccessLevelUnit(ctx, user, repo, unitType)
   377  	return testMode <= mode, err
   378  }
   379  
   380  // CanBeAssigned return true if user can be assigned to issue or pull requests in repo
   381  // Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
   382  func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
   383  	if user.IsOrganization() {
   384  		return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
   385  	}
   386  	perm, err := GetUserRepoPermission(ctx, repo, user)
   387  	if err != nil {
   388  		return false, err
   389  	}
   390  	return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) ||
   391  		perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil
   392  }
   393  
   394  // HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess"
   395  func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) {
   396  	var user *user_model.User
   397  	var err error
   398  	if userID > 0 {
   399  		user, err = user_model.GetUserByID(ctx, userID)
   400  		if err != nil {
   401  			return false, err
   402  		}
   403  	}
   404  	perm, err := GetUserRepoPermission(ctx, repo, user)
   405  	if err != nil {
   406  		return false, err
   407  	}
   408  	return perm.HasAnyUnitAccess(), nil
   409  }
   410  
   411  // getUsersWithAccessMode returns users that have at least given access mode to the repository.
   412  func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
   413  	if err = repo.LoadOwner(ctx); err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	e := db.GetEngine(ctx)
   418  	accesses := make([]*Access, 0, 10)
   419  	if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	// Leave a seat for owner itself to append later, but if owner is an organization
   424  	// and just waste 1 unit is cheaper than re-allocate memory once.
   425  	users := make([]*user_model.User, 0, len(accesses)+1)
   426  	if len(accesses) > 0 {
   427  		userIDs := make([]int64, len(accesses))
   428  		for i := 0; i < len(accesses); i++ {
   429  			userIDs[i] = accesses[i].UserID
   430  		}
   431  
   432  		if err = e.In("id", userIDs).Find(&users); err != nil {
   433  			return nil, err
   434  		}
   435  	}
   436  	if !repo.Owner.IsOrganization() {
   437  		users = append(users, repo.Owner)
   438  	}
   439  
   440  	return users, nil
   441  }
   442  
   443  // GetRepoReaders returns all users that have explicit read access or higher to the repository.
   444  func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
   445  	return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
   446  }
   447  
   448  // GetRepoWriters returns all users that have write access to the repository.
   449  func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
   450  	return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
   451  }
   452  
   453  // IsRepoReader returns true if user has explicit read access or higher to the repository.
   454  func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
   455  	if repo.OwnerID == userID {
   456  		return true, nil
   457  	}
   458  	return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
   459  }
   460  
   461  // CheckRepoUnitUser check whether user could visit the unit of this repository
   462  func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
   463  	if user != nil && user.IsAdmin {
   464  		return true
   465  	}
   466  	perm, err := GetUserRepoPermission(ctx, repo, user)
   467  	if err != nil {
   468  		log.Error("GetUserRepoPermission: %w", err)
   469  		return false
   470  	}
   471  
   472  	return perm.CanRead(unitType)
   473  }