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

Team permission allow different unit has different permission (#17811) * Team permission allow different unit has different permission * Finish the interface and the logic * Fix lint * Fix translation * align center for table cell content * Fix fixture * merge * Fix test * Add deprecated * Improve code * Add tooltip * Fix swagger * Fix newline * Fix tests * Fix tests * Fix test * Fix test * Max permission of external wiki and issues should be read * Move team units with limited max level below units table * Update label and column names * Some improvements * Fix lint * Some improvements * Fix template variables * Add permission docs * improve doc * Fix fixture * Fix bug * Fix some bug * fix * gofumpt * Integration test for migration (#18124) integrations: basic test for Gitea {dump,restore}-repo This is a first step for integration testing of DumpRepository and RestoreRepository. It: runs a Gitea server, dumps a repo via DumpRepository to the filesystem, restores the repo via RestoreRepository from the filesystem, dumps the restored repository to the filesystem, compares the first and second dump and expects them to be identical The verification is trivial and the goal is to add more tests for each topic of the dump. Signed-off-by: Loïc Dachary <loic@dachary.org> * Team permission allow different unit has different permission * Finish the interface and the logic * Fix lint * Fix translation * align center for table cell content * Fix fixture * merge * Fix test * Add deprecated * Improve code * Add tooltip * Fix swagger * Fix newline * Fix tests * Fix tests * Fix test * Fix test * Max permission of external wiki and issues should be read * Move team units with limited max level below units table * Update label and column names * Some improvements * Fix lint * Some improvements * Fix template variables * Add permission docs * improve doc * Fix fixture * Fix bug * Fix some bug * Fix bug Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Aravinth Manivannan <realaravinth@batsense.net>
2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  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.ID, repo.ID, 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(t *organization.Team) (err error) {
  63. ctx, committer, err := db.TxContext(db.DefaultContext)
  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(t *organization.Team) (err error) {
  75. if t.IncludesAllRepositories {
  76. return nil
  77. }
  78. ctx, committer, err := db.TxContext(db.DefaultContext)
  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.ID, repo.ID, 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. // HasRepository returns true if given repository belong to team.
  127. func HasRepository(t *organization.Team, repoID int64) bool {
  128. return organization.HasTeamRepo(db.DefaultContext, t.OrgID, t.ID, repoID)
  129. }
  130. // removeRepository removes a repository from a team and recalculates access
  131. // Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
  132. func removeRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
  133. e := db.GetEngine(ctx)
  134. if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
  135. return err
  136. }
  137. t.NumRepos--
  138. if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
  139. return err
  140. }
  141. // Don't need to recalculate when delete a repository from organization.
  142. if recalculate {
  143. if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
  144. return err
  145. }
  146. }
  147. teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
  148. if err != nil {
  149. return fmt.Errorf("getTeamUsersByTeamID: %w", err)
  150. }
  151. for _, teamUser := range teamUsers {
  152. has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
  153. if err != nil {
  154. return err
  155. } else if has {
  156. continue
  157. }
  158. if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
  159. return err
  160. }
  161. // Remove all IssueWatches a user has subscribed to in the repositories
  162. if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
  163. return err
  164. }
  165. }
  166. return nil
  167. }
  168. // RemoveRepository removes repository from team of organization.
  169. // If the team shall include all repositories the request is ignored.
  170. func RemoveRepository(t *organization.Team, repoID int64) error {
  171. if !HasRepository(t, repoID) {
  172. return nil
  173. }
  174. if t.IncludesAllRepositories {
  175. return nil
  176. }
  177. repo, err := repo_model.GetRepositoryByID(db.DefaultContext, repoID)
  178. if err != nil {
  179. return err
  180. }
  181. ctx, committer, err := db.TxContext(db.DefaultContext)
  182. if err != nil {
  183. return err
  184. }
  185. defer committer.Close()
  186. if err = removeRepository(ctx, t, repo, true); err != nil {
  187. return err
  188. }
  189. return committer.Commit()
  190. }
  191. // NewTeam creates a record of new team.
  192. // It's caller's responsibility to assign organization ID.
  193. func NewTeam(t *organization.Team) (err error) {
  194. if len(t.Name) == 0 {
  195. return util.NewInvalidArgumentErrorf("empty team name")
  196. }
  197. if err = organization.IsUsableTeamName(t.Name); err != nil {
  198. return err
  199. }
  200. has, err := db.GetEngine(db.DefaultContext).ID(t.OrgID).Get(new(user_model.User))
  201. if err != nil {
  202. return err
  203. }
  204. if !has {
  205. return organization.ErrOrgNotExist{ID: t.OrgID}
  206. }
  207. t.LowerName = strings.ToLower(t.Name)
  208. has, err = db.GetEngine(db.DefaultContext).
  209. Where("org_id=?", t.OrgID).
  210. And("lower_name=?", t.LowerName).
  211. Get(new(organization.Team))
  212. if err != nil {
  213. return err
  214. }
  215. if has {
  216. return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
  217. }
  218. ctx, committer, err := db.TxContext(db.DefaultContext)
  219. if err != nil {
  220. return err
  221. }
  222. defer committer.Close()
  223. if err = db.Insert(ctx, t); err != nil {
  224. return err
  225. }
  226. // insert units for team
  227. if len(t.Units) > 0 {
  228. for _, unit := range t.Units {
  229. unit.TeamID = t.ID
  230. }
  231. if err = db.Insert(ctx, &t.Units); err != nil {
  232. return err
  233. }
  234. }
  235. // Add all repositories to the team if it has access to all of them.
  236. if t.IncludesAllRepositories {
  237. err = addAllRepositories(ctx, t)
  238. if err != nil {
  239. return fmt.Errorf("addAllRepositories: %w", err)
  240. }
  241. }
  242. // Update organization number of teams.
  243. if _, err = db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
  244. return err
  245. }
  246. return committer.Commit()
  247. }
  248. // UpdateTeam updates information of team.
  249. func UpdateTeam(t *organization.Team, authChanged, includeAllChanged bool) (err error) {
  250. if len(t.Name) == 0 {
  251. return util.NewInvalidArgumentErrorf("empty team name")
  252. }
  253. if len(t.Description) > 255 {
  254. t.Description = t.Description[:255]
  255. }
  256. ctx, committer, err := db.TxContext(db.DefaultContext)
  257. if err != nil {
  258. return err
  259. }
  260. defer committer.Close()
  261. sess := db.GetEngine(ctx)
  262. t.LowerName = strings.ToLower(t.Name)
  263. has, err := sess.
  264. Where("org_id=?", t.OrgID).
  265. And("lower_name=?", t.LowerName).
  266. And("id!=?", t.ID).
  267. Get(new(organization.Team))
  268. if err != nil {
  269. return err
  270. } else if has {
  271. return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
  272. }
  273. if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
  274. "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
  275. return fmt.Errorf("update: %w", err)
  276. }
  277. // update units for team
  278. if len(t.Units) > 0 {
  279. for _, unit := range t.Units {
  280. unit.TeamID = t.ID
  281. }
  282. // Delete team-unit.
  283. if _, err := sess.
  284. Where("team_id=?", t.ID).
  285. Delete(new(organization.TeamUnit)); err != nil {
  286. return err
  287. }
  288. if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil {
  289. return err
  290. }
  291. }
  292. // Update access for team members if needed.
  293. if authChanged {
  294. if err = t.LoadRepositories(ctx); err != nil {
  295. return fmt.Errorf("LoadRepositories: %w", err)
  296. }
  297. for _, repo := range t.Repos {
  298. if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
  299. return fmt.Errorf("recalculateTeamAccesses: %w", err)
  300. }
  301. }
  302. }
  303. // Add all repositories to the team if it has access to all of them.
  304. if includeAllChanged && t.IncludesAllRepositories {
  305. err = addAllRepositories(ctx, t)
  306. if err != nil {
  307. return fmt.Errorf("addAllRepositories: %w", err)
  308. }
  309. }
  310. return committer.Commit()
  311. }
  312. // DeleteTeam deletes given team.
  313. // It's caller's responsibility to assign organization ID.
  314. func DeleteTeam(t *organization.Team) error {
  315. ctx, committer, err := db.TxContext(db.DefaultContext)
  316. if err != nil {
  317. return err
  318. }
  319. defer committer.Close()
  320. if err := t.LoadRepositories(ctx); err != nil {
  321. return err
  322. }
  323. if err := t.LoadMembers(ctx); err != nil {
  324. return err
  325. }
  326. // update branch protections
  327. {
  328. protections := make([]*git_model.ProtectedBranch, 0, 10)
  329. err := db.GetEngine(ctx).In("repo_id",
  330. builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})).
  331. Find(&protections)
  332. if err != nil {
  333. return fmt.Errorf("findProtectedBranches: %w", err)
  334. }
  335. for _, p := range protections {
  336. if err := git_model.RemoveTeamIDFromProtectedBranch(ctx, p, t.ID); err != nil {
  337. return err
  338. }
  339. }
  340. }
  341. if !t.IncludesAllRepositories {
  342. if err := removeAllRepositories(ctx, t); err != nil {
  343. return err
  344. }
  345. }
  346. if err := db.DeleteBeans(ctx,
  347. &organization.Team{ID: t.ID},
  348. &organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
  349. &organization.TeamUnit{TeamID: t.ID},
  350. &organization.TeamInvite{TeamID: t.ID},
  351. ); err != nil {
  352. return err
  353. }
  354. // Update organization number of teams.
  355. if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
  356. return err
  357. }
  358. return committer.Commit()
  359. }
  360. // AddTeamMember adds new membership of given team to given organization,
  361. // the user will have membership to given organization automatically when needed.
  362. func AddTeamMember(team *organization.Team, userID int64) error {
  363. isAlreadyMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, userID)
  364. if err != nil || isAlreadyMember {
  365. return err
  366. }
  367. if err := organization.AddOrgUser(team.OrgID, userID); err != nil {
  368. return err
  369. }
  370. ctx, committer, err := db.TxContext(db.DefaultContext)
  371. if err != nil {
  372. return err
  373. }
  374. defer committer.Close()
  375. // check in transaction
  376. isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
  377. if err != nil || isAlreadyMember {
  378. return err
  379. }
  380. sess := db.GetEngine(ctx)
  381. if err := db.Insert(ctx, &organization.TeamUser{
  382. UID: userID,
  383. OrgID: team.OrgID,
  384. TeamID: team.ID,
  385. }); err != nil {
  386. return err
  387. } else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil {
  388. return err
  389. }
  390. team.NumMembers++
  391. // Give access to team repositories.
  392. // update exist access if mode become bigger
  393. subQuery := builder.Select("repo_id").From("team_repo").
  394. Where(builder.Eq{"team_id": team.ID})
  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. // for not exist access
  403. var repoIDs []int64
  404. accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
  405. if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
  406. return fmt.Errorf("select id accesses: %w", err)
  407. }
  408. accesses := make([]*access_model.Access, 0, 100)
  409. for i, repoID := range repoIDs {
  410. accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
  411. if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
  412. if err = db.Insert(ctx, accesses); err != nil {
  413. return fmt.Errorf("insert new user accesses: %w", err)
  414. }
  415. accesses = accesses[:0]
  416. }
  417. }
  418. if err := committer.Commit(); err != nil {
  419. return err
  420. }
  421. committer.Close()
  422. // this behaviour may spend much time so run it in a goroutine
  423. // FIXME: Update watch repos batchly
  424. if setting.Service.AutoWatchNewRepos {
  425. // Get team and its repositories.
  426. if err := team.LoadRepositories(db.DefaultContext); err != nil {
  427. log.Error("getRepositories failed: %v", err)
  428. }
  429. go func(repos []*repo_model.Repository) {
  430. for _, repo := range repos {
  431. if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
  432. log.Error("watch repo failed: %v", err)
  433. }
  434. }
  435. }(team.Repos)
  436. }
  437. return nil
  438. }
  439. func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
  440. e := db.GetEngine(ctx)
  441. isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
  442. if err != nil || !isMember {
  443. return err
  444. }
  445. // Check if the user to delete is the last member in owner team.
  446. if team.IsOwnerTeam() && team.NumMembers == 1 {
  447. return organization.ErrLastOrgOwner{UID: userID}
  448. }
  449. team.NumMembers--
  450. if err := team.LoadRepositories(ctx); err != nil {
  451. return err
  452. }
  453. if _, err := e.Delete(&organization.TeamUser{
  454. UID: userID,
  455. OrgID: team.OrgID,
  456. TeamID: team.ID,
  457. }); err != nil {
  458. return err
  459. } else if _, err = e.
  460. ID(team.ID).
  461. Cols("num_members").
  462. Update(team); err != nil {
  463. return err
  464. }
  465. // Delete access to team repositories.
  466. for _, repo := range team.Repos {
  467. if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
  468. return err
  469. }
  470. // Remove watches from now unaccessible
  471. if err := reconsiderWatches(ctx, repo, userID); err != nil {
  472. return err
  473. }
  474. // Remove issue assignments from now unaccessible
  475. if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
  476. return err
  477. }
  478. }
  479. // Check if the user is a member of any team in the organization.
  480. if count, err := e.Count(&organization.TeamUser{
  481. UID: userID,
  482. OrgID: team.OrgID,
  483. }); err != nil {
  484. return err
  485. } else if count == 0 {
  486. return removeOrgUser(ctx, team.OrgID, userID)
  487. }
  488. return nil
  489. }
  490. // RemoveTeamMember removes member from given team of given organization.
  491. func RemoveTeamMember(team *organization.Team, userID int64) error {
  492. ctx, committer, err := db.TxContext(db.DefaultContext)
  493. if err != nil {
  494. return err
  495. }
  496. defer committer.Close()
  497. if err := removeTeamMember(ctx, team, userID); err != nil {
  498. return err
  499. }
  500. return committer.Commit()
  501. }