code.gitea.io/gitea@v1.22.3/models/org_team.go (about) 1 // Copyright 2018 The Gitea Authors. All rights reserved. 2 // Copyright 2016 The Gogs Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package models 6 7 import ( 8 "context" 9 "fmt" 10 "strings" 11 12 "code.gitea.io/gitea/models/db" 13 git_model "code.gitea.io/gitea/models/git" 14 issues_model "code.gitea.io/gitea/models/issues" 15 "code.gitea.io/gitea/models/organization" 16 access_model "code.gitea.io/gitea/models/perm/access" 17 repo_model "code.gitea.io/gitea/models/repo" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/setting" 21 "code.gitea.io/gitea/modules/util" 22 23 "xorm.io/builder" 24 ) 25 26 func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) { 27 if err = organization.AddTeamRepo(ctx, t.OrgID, t.ID, repo.ID); err != nil { 28 return err 29 } 30 31 if err = organization.IncrTeamRepoNum(ctx, t.ID); err != nil { 32 return fmt.Errorf("update team: %w", err) 33 } 34 35 t.NumRepos++ 36 37 if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { 38 return fmt.Errorf("recalculateAccesses: %w", err) 39 } 40 41 // Make all team members watch this repo if enabled in global settings 42 if setting.Service.AutoWatchNewRepos { 43 if err = t.LoadMembers(ctx); err != nil { 44 return fmt.Errorf("getMembers: %w", err) 45 } 46 for _, u := range t.Members { 47 if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil { 48 return fmt.Errorf("watchRepo: %w", err) 49 } 50 } 51 } 52 53 return nil 54 } 55 56 // addAllRepositories adds all repositories to the team. 57 // If the team already has some repositories they will be left unchanged. 58 func addAllRepositories(ctx context.Context, t *organization.Team) error { 59 orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID) 60 if err != nil { 61 return fmt.Errorf("get org repos: %w", err) 62 } 63 64 for _, repo := range orgRepos { 65 if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) { 66 if err := AddRepository(ctx, t, repo); err != nil { 67 return fmt.Errorf("AddRepository: %w", err) 68 } 69 } 70 } 71 72 return nil 73 } 74 75 // AddAllRepositories adds all repositories to the team 76 func AddAllRepositories(ctx context.Context, t *organization.Team) (err error) { 77 ctx, committer, err := db.TxContext(ctx) 78 if err != nil { 79 return err 80 } 81 defer committer.Close() 82 83 if err = addAllRepositories(ctx, t); err != nil { 84 return err 85 } 86 87 return committer.Commit() 88 } 89 90 // RemoveAllRepositories removes all repositories from team and recalculates access 91 func RemoveAllRepositories(ctx context.Context, t *organization.Team) (err error) { 92 if t.IncludesAllRepositories { 93 return nil 94 } 95 96 ctx, committer, err := db.TxContext(ctx) 97 if err != nil { 98 return err 99 } 100 defer committer.Close() 101 102 if err = removeAllRepositories(ctx, t); err != nil { 103 return err 104 } 105 106 return committer.Commit() 107 } 108 109 // removeAllRepositories removes all repositories from team and recalculates access 110 // Note: Shall not be called if team includes all repositories 111 func removeAllRepositories(ctx context.Context, t *organization.Team) (err error) { 112 e := db.GetEngine(ctx) 113 // Delete all accesses. 114 for _, repo := range t.Repos { 115 if err := access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil { 116 return err 117 } 118 119 // Remove watches from all users and now unaccessible repos 120 for _, user := range t.Members { 121 has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo) 122 if err != nil { 123 return err 124 } else if has { 125 continue 126 } 127 128 if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { 129 return err 130 } 131 132 // Remove all IssueWatches a user has subscribed to in the repositories 133 if err = issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil { 134 return err 135 } 136 } 137 } 138 139 // Delete team-repo 140 if _, err := e. 141 Where("team_id=?", t.ID). 142 Delete(new(organization.TeamRepo)); err != nil { 143 return err 144 } 145 146 t.NumRepos = 0 147 if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil { 148 return err 149 } 150 151 return nil 152 } 153 154 // NewTeam creates a record of new team. 155 // It's caller's responsibility to assign organization ID. 156 func NewTeam(ctx context.Context, t *organization.Team) (err error) { 157 if len(t.Name) == 0 { 158 return util.NewInvalidArgumentErrorf("empty team name") 159 } 160 161 if err = organization.IsUsableTeamName(t.Name); err != nil { 162 return err 163 } 164 165 has, err := db.ExistByID[user_model.User](ctx, t.OrgID) 166 if err != nil { 167 return err 168 } 169 if !has { 170 return organization.ErrOrgNotExist{ID: t.OrgID} 171 } 172 173 t.LowerName = strings.ToLower(t.Name) 174 has, err = db.Exist[organization.Team](ctx, builder.Eq{ 175 "org_id": t.OrgID, 176 "lower_name": t.LowerName, 177 }) 178 if err != nil { 179 return err 180 } 181 if has { 182 return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName} 183 } 184 185 ctx, committer, err := db.TxContext(ctx) 186 if err != nil { 187 return err 188 } 189 defer committer.Close() 190 191 if err = db.Insert(ctx, t); err != nil { 192 return err 193 } 194 195 // insert units for team 196 if len(t.Units) > 0 { 197 for _, unit := range t.Units { 198 unit.TeamID = t.ID 199 } 200 if err = db.Insert(ctx, &t.Units); err != nil { 201 return err 202 } 203 } 204 205 // Add all repositories to the team if it has access to all of them. 206 if t.IncludesAllRepositories { 207 err = addAllRepositories(ctx, t) 208 if err != nil { 209 return fmt.Errorf("addAllRepositories: %w", err) 210 } 211 } 212 213 // Update organization number of teams. 214 if _, err = db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil { 215 return err 216 } 217 return committer.Commit() 218 } 219 220 // UpdateTeam updates information of team. 221 func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeAllChanged bool) (err error) { 222 if len(t.Name) == 0 { 223 return util.NewInvalidArgumentErrorf("empty team name") 224 } 225 226 if len(t.Description) > 255 { 227 t.Description = t.Description[:255] 228 } 229 230 ctx, committer, err := db.TxContext(ctx) 231 if err != nil { 232 return err 233 } 234 defer committer.Close() 235 236 t.LowerName = strings.ToLower(t.Name) 237 has, err := db.Exist[organization.Team](ctx, builder.Eq{ 238 "org_id": t.OrgID, 239 "lower_name": t.LowerName, 240 }.And(builder.Neq{"id": t.ID}), 241 ) 242 if err != nil { 243 return err 244 } else if has { 245 return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName} 246 } 247 248 sess := db.GetEngine(ctx) 249 if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description", 250 "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil { 251 return fmt.Errorf("update: %w", err) 252 } 253 254 // update units for team 255 if len(t.Units) > 0 { 256 for _, unit := range t.Units { 257 unit.TeamID = t.ID 258 } 259 // Delete team-unit. 260 if _, err := sess. 261 Where("team_id=?", t.ID). 262 Delete(new(organization.TeamUnit)); err != nil { 263 return err 264 } 265 if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil { 266 return err 267 } 268 } 269 270 // Update access for team members if needed. 271 if authChanged { 272 if err = t.LoadRepositories(ctx); err != nil { 273 return fmt.Errorf("LoadRepositories: %w", err) 274 } 275 276 for _, repo := range t.Repos { 277 if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { 278 return fmt.Errorf("recalculateTeamAccesses: %w", err) 279 } 280 } 281 } 282 283 // Add all repositories to the team if it has access to all of them. 284 if includeAllChanged && t.IncludesAllRepositories { 285 err = addAllRepositories(ctx, t) 286 if err != nil { 287 return fmt.Errorf("addAllRepositories: %w", err) 288 } 289 } 290 291 return committer.Commit() 292 } 293 294 // DeleteTeam deletes given team. 295 // It's caller's responsibility to assign organization ID. 296 func DeleteTeam(ctx context.Context, t *organization.Team) error { 297 ctx, committer, err := db.TxContext(ctx) 298 if err != nil { 299 return err 300 } 301 defer committer.Close() 302 303 if err := t.LoadRepositories(ctx); err != nil { 304 return err 305 } 306 307 if err := t.LoadMembers(ctx); err != nil { 308 return err 309 } 310 311 // update branch protections 312 { 313 protections := make([]*git_model.ProtectedBranch, 0, 10) 314 err := db.GetEngine(ctx).In("repo_id", 315 builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})). 316 Find(&protections) 317 if err != nil { 318 return fmt.Errorf("findProtectedBranches: %w", err) 319 } 320 for _, p := range protections { 321 if err := git_model.RemoveTeamIDFromProtectedBranch(ctx, p, t.ID); err != nil { 322 return err 323 } 324 } 325 } 326 327 if !t.IncludesAllRepositories { 328 if err := removeAllRepositories(ctx, t); err != nil { 329 return err 330 } 331 } 332 333 if err := db.DeleteBeans(ctx, 334 &organization.Team{ID: t.ID}, 335 &organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID}, 336 &organization.TeamUnit{TeamID: t.ID}, 337 &organization.TeamInvite{TeamID: t.ID}, 338 &issues_model.Review{Type: issues_model.ReviewTypeRequest, ReviewerTeamID: t.ID}, // batch delete the binding relationship between team and PR (request review from team) 339 ); err != nil { 340 return err 341 } 342 343 for _, tm := range t.Members { 344 if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil { 345 return err 346 } 347 } 348 349 // Update organization number of teams. 350 if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil { 351 return err 352 } 353 354 return committer.Commit() 355 } 356 357 // AddTeamMember adds new membership of given team to given organization, 358 // the user will have membership to given organization automatically when needed. 359 func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { 360 if user_model.IsUserBlockedBy(ctx, user, team.OrgID) { 361 return user_model.ErrBlockedUser 362 } 363 364 isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) 365 if err != nil || isAlreadyMember { 366 return err 367 } 368 369 if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil { 370 return err 371 } 372 373 err = db.WithTx(ctx, func(ctx context.Context) error { 374 // check in transaction 375 isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) 376 if err != nil || isAlreadyMember { 377 return err 378 } 379 380 sess := db.GetEngine(ctx) 381 382 if err := db.Insert(ctx, &organization.TeamUser{ 383 UID: user.ID, 384 OrgID: team.OrgID, 385 TeamID: team.ID, 386 }); err != nil { 387 return err 388 } else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil { 389 return err 390 } 391 392 team.NumMembers++ 393 394 // Give access to team repositories. 395 // update exist access if mode become bigger 396 subQuery := builder.Select("repo_id").From("team_repo"). 397 Where(builder.Eq{"team_id": team.ID}) 398 399 if _, err := sess.Where("user_id=?", user.ID). 400 In("repo_id", subQuery). 401 And("mode < ?", team.AccessMode). 402 SetExpr("mode", team.AccessMode). 403 Update(new(access_model.Access)); err != nil { 404 return fmt.Errorf("update user accesses: %w", err) 405 } 406 407 // for not exist access 408 var repoIDs []int64 409 accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID}) 410 if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil { 411 return fmt.Errorf("select id accesses: %w", err) 412 } 413 414 accesses := make([]*access_model.Access, 0, 100) 415 for i, repoID := range repoIDs { 416 accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode}) 417 if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 { 418 if err = db.Insert(ctx, accesses); err != nil { 419 return fmt.Errorf("insert new user accesses: %w", err) 420 } 421 accesses = accesses[:0] 422 } 423 } 424 return nil 425 }) 426 if err != nil { 427 return err 428 } 429 430 // this behaviour may spend much time so run it in a goroutine 431 // FIXME: Update watch repos batchly 432 if setting.Service.AutoWatchNewRepos { 433 // Get team and its repositories. 434 if err := team.LoadRepositories(ctx); err != nil { 435 log.Error("team.LoadRepositories failed: %v", err) 436 } 437 438 // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment 439 go func(repos []*repo_model.Repository) { 440 for _, repo := range repos { 441 if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil { 442 log.Error("watch repo failed: %v", err) 443 } 444 } 445 }(team.Repos) 446 } 447 448 return nil 449 } 450 451 func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { 452 e := db.GetEngine(ctx) 453 isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) 454 if err != nil || !isMember { 455 return err 456 } 457 458 // Check if the user to delete is the last member in owner team. 459 if team.IsOwnerTeam() && team.NumMembers == 1 { 460 return organization.ErrLastOrgOwner{UID: user.ID} 461 } 462 463 team.NumMembers-- 464 465 if err := team.LoadRepositories(ctx); err != nil { 466 return err 467 } 468 469 if _, err := e.Delete(&organization.TeamUser{ 470 UID: user.ID, 471 OrgID: team.OrgID, 472 TeamID: team.ID, 473 }); err != nil { 474 return err 475 } else if _, err = e. 476 ID(team.ID). 477 Cols("num_members"). 478 Update(team); err != nil { 479 return err 480 } 481 482 // Delete access to team repositories. 483 for _, repo := range team.Repos { 484 if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil { 485 return err 486 } 487 488 // Remove watches from now unaccessible 489 if err := ReconsiderWatches(ctx, repo, user); err != nil { 490 return err 491 } 492 493 // Remove issue assignments from now unaccessible 494 if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil { 495 return err 496 } 497 } 498 499 return removeInvalidOrgUser(ctx, team.OrgID, user) 500 } 501 502 func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error { 503 // Check if the user is a member of any team in the organization. 504 if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{ 505 UID: user.ID, 506 OrgID: orgID, 507 }); err != nil { 508 return err 509 } else if count == 0 { 510 org, err := organization.GetOrgByID(ctx, orgID) 511 if err != nil { 512 return err 513 } 514 515 return RemoveOrgUser(ctx, org, user) 516 } 517 return nil 518 } 519 520 // RemoveTeamMember removes member from given team of given organization. 521 func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { 522 ctx, committer, err := db.TxContext(ctx) 523 if err != nil { 524 return err 525 } 526 defer committer.Close() 527 if err := removeTeamMember(ctx, team, user); err != nil { 528 return err 529 } 530 return committer.Commit() 531 } 532 533 func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { 534 if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { 535 return err 536 } 537 538 if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}). 539 In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). 540 Delete(&issues_model.IssueAssignees{}); err != nil { 541 return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err) 542 } 543 return nil 544 } 545 546 func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { 547 if has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo); err != nil || has { 548 return err 549 } 550 if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { 551 return err 552 } 553 554 // Remove all IssueWatches a user has subscribed to in the repository 555 return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID) 556 }