code.gitea.io/gitea@v1.21.7/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.ID, repo.ID, 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.HasAccess(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.ID, repo.ID, 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.GetEngine(ctx).ID(t.OrgID).Get(new(user_model.User)) 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.GetEngine(ctx). 175 Where("org_id=?", t.OrgID). 176 And("lower_name=?", t.LowerName). 177 Get(new(organization.Team)) 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 sess := db.GetEngine(ctx) 236 237 t.LowerName = strings.ToLower(t.Name) 238 has, err := sess. 239 Where("org_id=?", t.OrgID). 240 And("lower_name=?", t.LowerName). 241 And("id!=?", t.ID). 242 Get(new(organization.Team)) 243 if err != nil { 244 return err 245 } else if has { 246 return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName} 247 } 248 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, tm.ID, t.OrgID); 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, userID int64) error { 360 isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) 361 if err != nil || isAlreadyMember { 362 return err 363 } 364 365 if err := organization.AddOrgUser(team.OrgID, userID); err != nil { 366 return err 367 } 368 369 err = db.WithTx(ctx, func(ctx context.Context) error { 370 // check in transaction 371 isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) 372 if err != nil || isAlreadyMember { 373 return err 374 } 375 376 sess := db.GetEngine(ctx) 377 378 if err := db.Insert(ctx, &organization.TeamUser{ 379 UID: userID, 380 OrgID: team.OrgID, 381 TeamID: team.ID, 382 }); err != nil { 383 return err 384 } else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil { 385 return err 386 } 387 388 team.NumMembers++ 389 390 // Give access to team repositories. 391 // update exist access if mode become bigger 392 subQuery := builder.Select("repo_id").From("team_repo"). 393 Where(builder.Eq{"team_id": team.ID}) 394 395 if _, err := sess.Where("user_id=?", userID). 396 In("repo_id", subQuery). 397 And("mode < ?", team.AccessMode). 398 SetExpr("mode", team.AccessMode). 399 Update(new(access_model.Access)); err != nil { 400 return fmt.Errorf("update user accesses: %w", err) 401 } 402 403 // for not exist access 404 var repoIDs []int64 405 accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID}) 406 if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil { 407 return fmt.Errorf("select id accesses: %w", err) 408 } 409 410 accesses := make([]*access_model.Access, 0, 100) 411 for i, repoID := range repoIDs { 412 accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode}) 413 if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 { 414 if err = db.Insert(ctx, accesses); err != nil { 415 return fmt.Errorf("insert new user accesses: %w", err) 416 } 417 accesses = accesses[:0] 418 } 419 } 420 return nil 421 }) 422 if err != nil { 423 return err 424 } 425 426 // this behaviour may spend much time so run it in a goroutine 427 // FIXME: Update watch repos batchly 428 if setting.Service.AutoWatchNewRepos { 429 // Get team and its repositories. 430 if err := team.LoadRepositories(ctx); err != nil { 431 log.Error("team.LoadRepositories failed: %v", err) 432 } 433 // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment 434 go func(repos []*repo_model.Repository) { 435 for _, repo := range repos { 436 if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil { 437 log.Error("watch repo failed: %v", err) 438 } 439 } 440 }(team.Repos) 441 } 442 443 return nil 444 } 445 446 func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error { 447 e := db.GetEngine(ctx) 448 isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) 449 if err != nil || !isMember { 450 return err 451 } 452 453 // Check if the user to delete is the last member in owner team. 454 if team.IsOwnerTeam() && team.NumMembers == 1 { 455 return organization.ErrLastOrgOwner{UID: userID} 456 } 457 458 team.NumMembers-- 459 460 if err := team.LoadRepositories(ctx); err != nil { 461 return err 462 } 463 464 if _, err := e.Delete(&organization.TeamUser{ 465 UID: userID, 466 OrgID: team.OrgID, 467 TeamID: team.ID, 468 }); err != nil { 469 return err 470 } else if _, err = e. 471 ID(team.ID). 472 Cols("num_members"). 473 Update(team); err != nil { 474 return err 475 } 476 477 // Delete access to team repositories. 478 for _, repo := range team.Repos { 479 if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil { 480 return err 481 } 482 483 // Remove watches from now unaccessible 484 if err := ReconsiderWatches(ctx, repo, userID); err != nil { 485 return err 486 } 487 488 // Remove issue assignments from now unaccessible 489 if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { 490 return err 491 } 492 } 493 494 return removeInvalidOrgUser(ctx, userID, team.OrgID) 495 } 496 497 func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error { 498 // Check if the user is a member of any team in the organization. 499 if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{ 500 UID: userID, 501 OrgID: orgID, 502 }); err != nil { 503 return err 504 } else if count == 0 { 505 return removeOrgUser(ctx, orgID, userID) 506 } 507 return nil 508 } 509 510 // RemoveTeamMember removes member from given team of given organization. 511 func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error { 512 ctx, committer, err := db.TxContext(ctx) 513 if err != nil { 514 return err 515 } 516 defer committer.Close() 517 if err := removeTeamMember(ctx, team, userID); err != nil { 518 return err 519 } 520 return committer.Commit() 521 } 522 523 func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { 524 user, err := user_model.GetUserByID(ctx, uid) 525 if err != nil { 526 return err 527 } 528 529 if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { 530 return err 531 } 532 533 if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). 534 In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). 535 Delete(&issues_model.IssueAssignees{}); err != nil { 536 return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) 537 } 538 return nil 539 } 540 541 func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { 542 if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { 543 return err 544 } 545 if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { 546 return err 547 } 548 549 // Remove all IssueWatches a user has subscribed to in the repository 550 return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) 551 }