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 }