You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

org_team.go 15KB

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