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.

team.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package org
  6. import (
  7. "net/http"
  8. "code.gitea.io/gitea/models"
  9. "code.gitea.io/gitea/modules/context"
  10. "code.gitea.io/gitea/modules/convert"
  11. "code.gitea.io/gitea/modules/log"
  12. api "code.gitea.io/gitea/modules/structs"
  13. "code.gitea.io/gitea/modules/web"
  14. "code.gitea.io/gitea/routers/api/v1/user"
  15. "code.gitea.io/gitea/routers/api/v1/utils"
  16. )
  17. // ListTeams list all the teams of an organization
  18. func ListTeams(ctx *context.APIContext) {
  19. // swagger:operation GET /orgs/{org}/teams organization orgListTeams
  20. // ---
  21. // summary: List an organization's teams
  22. // produces:
  23. // - application/json
  24. // parameters:
  25. // - name: org
  26. // in: path
  27. // description: name of the organization
  28. // type: string
  29. // required: true
  30. // - name: page
  31. // in: query
  32. // description: page number of results to return (1-based)
  33. // type: integer
  34. // - name: limit
  35. // in: query
  36. // description: page size of results
  37. // type: integer
  38. // responses:
  39. // "200":
  40. // "$ref": "#/responses/TeamList"
  41. teams, count, err := models.SearchTeam(&models.SearchTeamOptions{
  42. ListOptions: utils.GetListOptions(ctx),
  43. OrgID: ctx.Org.Organization.ID,
  44. })
  45. if err != nil {
  46. ctx.Error(http.StatusInternalServerError, "LoadTeams", err)
  47. return
  48. }
  49. apiTeams := make([]*api.Team, len(teams))
  50. for i := range teams {
  51. if err := teams[i].GetUnits(); err != nil {
  52. ctx.Error(http.StatusInternalServerError, "GetUnits", err)
  53. return
  54. }
  55. apiTeams[i] = convert.ToTeam(teams[i])
  56. }
  57. ctx.SetTotalCountHeader(count)
  58. ctx.JSON(http.StatusOK, apiTeams)
  59. }
  60. // ListUserTeams list all the teams a user belongs to
  61. func ListUserTeams(ctx *context.APIContext) {
  62. // swagger:operation GET /user/teams user userListTeams
  63. // ---
  64. // summary: List all the teams a user belongs to
  65. // produces:
  66. // - application/json
  67. // parameters:
  68. // - name: page
  69. // in: query
  70. // description: page number of results to return (1-based)
  71. // type: integer
  72. // - name: limit
  73. // in: query
  74. // description: page size of results
  75. // type: integer
  76. // responses:
  77. // "200":
  78. // "$ref": "#/responses/TeamList"
  79. teams, count, err := models.SearchTeam(&models.SearchTeamOptions{
  80. ListOptions: utils.GetListOptions(ctx),
  81. UserID: ctx.User.ID,
  82. })
  83. if err != nil {
  84. ctx.Error(http.StatusInternalServerError, "GetUserTeams", err)
  85. return
  86. }
  87. cache := make(map[int64]*api.Organization)
  88. apiTeams := make([]*api.Team, len(teams))
  89. for i := range teams {
  90. apiOrg, ok := cache[teams[i].OrgID]
  91. if !ok {
  92. org, err := models.GetUserByID(teams[i].OrgID)
  93. if err != nil {
  94. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  95. return
  96. }
  97. apiOrg = convert.ToOrganization(org)
  98. cache[teams[i].OrgID] = apiOrg
  99. }
  100. apiTeams[i] = convert.ToTeam(teams[i])
  101. apiTeams[i].Organization = apiOrg
  102. }
  103. ctx.SetTotalCountHeader(count)
  104. ctx.JSON(http.StatusOK, apiTeams)
  105. }
  106. // GetTeam api for get a team
  107. func GetTeam(ctx *context.APIContext) {
  108. // swagger:operation GET /teams/{id} organization orgGetTeam
  109. // ---
  110. // summary: Get a team
  111. // produces:
  112. // - application/json
  113. // parameters:
  114. // - name: id
  115. // in: path
  116. // description: id of the team to get
  117. // type: integer
  118. // format: int64
  119. // required: true
  120. // responses:
  121. // "200":
  122. // "$ref": "#/responses/Team"
  123. ctx.JSON(http.StatusOK, convert.ToTeam(ctx.Org.Team))
  124. }
  125. // CreateTeam api for create a team
  126. func CreateTeam(ctx *context.APIContext) {
  127. // swagger:operation POST /orgs/{org}/teams organization orgCreateTeam
  128. // ---
  129. // summary: Create a team
  130. // consumes:
  131. // - application/json
  132. // produces:
  133. // - application/json
  134. // parameters:
  135. // - name: org
  136. // in: path
  137. // description: name of the organization
  138. // type: string
  139. // required: true
  140. // - name: body
  141. // in: body
  142. // schema:
  143. // "$ref": "#/definitions/CreateTeamOption"
  144. // responses:
  145. // "201":
  146. // "$ref": "#/responses/Team"
  147. // "422":
  148. // "$ref": "#/responses/validationError"
  149. form := web.GetForm(ctx).(*api.CreateTeamOption)
  150. team := &models.Team{
  151. OrgID: ctx.Org.Organization.ID,
  152. Name: form.Name,
  153. Description: form.Description,
  154. IncludesAllRepositories: form.IncludesAllRepositories,
  155. CanCreateOrgRepo: form.CanCreateOrgRepo,
  156. Authorize: models.ParseAccessMode(form.Permission),
  157. }
  158. unitTypes := models.FindUnitTypes(form.Units...)
  159. if team.Authorize < models.AccessModeOwner {
  160. var units = make([]*models.TeamUnit, 0, len(form.Units))
  161. for _, tp := range unitTypes {
  162. units = append(units, &models.TeamUnit{
  163. OrgID: ctx.Org.Organization.ID,
  164. Type: tp,
  165. })
  166. }
  167. team.Units = units
  168. }
  169. if err := models.NewTeam(team); err != nil {
  170. if models.IsErrTeamAlreadyExist(err) {
  171. ctx.Error(http.StatusUnprocessableEntity, "", err)
  172. } else {
  173. ctx.Error(http.StatusInternalServerError, "NewTeam", err)
  174. }
  175. return
  176. }
  177. ctx.JSON(http.StatusCreated, convert.ToTeam(team))
  178. }
  179. // EditTeam api for edit a team
  180. func EditTeam(ctx *context.APIContext) {
  181. // swagger:operation PATCH /teams/{id} organization orgEditTeam
  182. // ---
  183. // summary: Edit a team
  184. // consumes:
  185. // - application/json
  186. // produces:
  187. // - application/json
  188. // parameters:
  189. // - name: id
  190. // in: path
  191. // description: id of the team to edit
  192. // type: integer
  193. // required: true
  194. // - name: body
  195. // in: body
  196. // schema:
  197. // "$ref": "#/definitions/EditTeamOption"
  198. // responses:
  199. // "200":
  200. // "$ref": "#/responses/Team"
  201. form := web.GetForm(ctx).(*api.EditTeamOption)
  202. team := ctx.Org.Team
  203. if err := team.GetUnits(); err != nil {
  204. ctx.InternalServerError(err)
  205. return
  206. }
  207. if form.CanCreateOrgRepo != nil {
  208. team.CanCreateOrgRepo = *form.CanCreateOrgRepo
  209. }
  210. if len(form.Name) > 0 {
  211. team.Name = form.Name
  212. }
  213. if form.Description != nil {
  214. team.Description = *form.Description
  215. }
  216. isAuthChanged := false
  217. isIncludeAllChanged := false
  218. if !team.IsOwnerTeam() && len(form.Permission) != 0 {
  219. // Validate permission level.
  220. auth := models.ParseAccessMode(form.Permission)
  221. if team.Authorize != auth {
  222. isAuthChanged = true
  223. team.Authorize = auth
  224. }
  225. if form.IncludesAllRepositories != nil {
  226. isIncludeAllChanged = true
  227. team.IncludesAllRepositories = *form.IncludesAllRepositories
  228. }
  229. }
  230. if team.Authorize < models.AccessModeOwner {
  231. if len(form.Units) > 0 {
  232. var units = make([]*models.TeamUnit, 0, len(form.Units))
  233. unitTypes := models.FindUnitTypes(form.Units...)
  234. for _, tp := range unitTypes {
  235. units = append(units, &models.TeamUnit{
  236. OrgID: ctx.Org.Team.OrgID,
  237. Type: tp,
  238. })
  239. }
  240. team.Units = units
  241. }
  242. }
  243. if err := models.UpdateTeam(team, isAuthChanged, isIncludeAllChanged); err != nil {
  244. ctx.Error(http.StatusInternalServerError, "EditTeam", err)
  245. return
  246. }
  247. ctx.JSON(http.StatusOK, convert.ToTeam(team))
  248. }
  249. // DeleteTeam api for delete a team
  250. func DeleteTeam(ctx *context.APIContext) {
  251. // swagger:operation DELETE /teams/{id} organization orgDeleteTeam
  252. // ---
  253. // summary: Delete a team
  254. // parameters:
  255. // - name: id
  256. // in: path
  257. // description: id of the team to delete
  258. // type: integer
  259. // format: int64
  260. // required: true
  261. // responses:
  262. // "204":
  263. // description: team deleted
  264. if err := models.DeleteTeam(ctx.Org.Team); err != nil {
  265. ctx.Error(http.StatusInternalServerError, "DeleteTeam", err)
  266. return
  267. }
  268. ctx.Status(http.StatusNoContent)
  269. }
  270. // GetTeamMembers api for get a team's members
  271. func GetTeamMembers(ctx *context.APIContext) {
  272. // swagger:operation GET /teams/{id}/members organization orgListTeamMembers
  273. // ---
  274. // summary: List a team's members
  275. // produces:
  276. // - application/json
  277. // parameters:
  278. // - name: id
  279. // in: path
  280. // description: id of the team
  281. // type: integer
  282. // format: int64
  283. // required: true
  284. // - name: page
  285. // in: query
  286. // description: page number of results to return (1-based)
  287. // type: integer
  288. // - name: limit
  289. // in: query
  290. // description: page size of results
  291. // type: integer
  292. // responses:
  293. // "200":
  294. // "$ref": "#/responses/UserList"
  295. isMember, err := models.IsOrganizationMember(ctx.Org.Team.OrgID, ctx.User.ID)
  296. if err != nil {
  297. ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
  298. return
  299. } else if !isMember && !ctx.User.IsAdmin {
  300. ctx.NotFound()
  301. return
  302. }
  303. if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{
  304. ListOptions: utils.GetListOptions(ctx),
  305. }); err != nil {
  306. ctx.Error(http.StatusInternalServerError, "GetTeamMembers", err)
  307. return
  308. }
  309. members := make([]*api.User, len(ctx.Org.Team.Members))
  310. for i, member := range ctx.Org.Team.Members {
  311. members[i] = convert.ToUser(member, ctx.User)
  312. }
  313. ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers))
  314. ctx.JSON(http.StatusOK, members)
  315. }
  316. // GetTeamMember api for get a particular member of team
  317. func GetTeamMember(ctx *context.APIContext) {
  318. // swagger:operation GET /teams/{id}/members/{username} organization orgListTeamMember
  319. // ---
  320. // summary: List a particular member of team
  321. // produces:
  322. // - application/json
  323. // parameters:
  324. // - name: id
  325. // in: path
  326. // description: id of the team
  327. // type: integer
  328. // format: int64
  329. // required: true
  330. // - name: username
  331. // in: path
  332. // description: username of the member to list
  333. // type: string
  334. // required: true
  335. // responses:
  336. // "200":
  337. // "$ref": "#/responses/User"
  338. // "404":
  339. // "$ref": "#/responses/notFound"
  340. u := user.GetUserByParams(ctx)
  341. if ctx.Written() {
  342. return
  343. }
  344. teamID := ctx.ParamsInt64("teamid")
  345. isTeamMember, err := models.IsUserInTeams(u.ID, []int64{teamID})
  346. if err != nil {
  347. ctx.Error(http.StatusInternalServerError, "IsUserInTeams", err)
  348. return
  349. } else if !isTeamMember {
  350. ctx.NotFound()
  351. return
  352. }
  353. ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
  354. }
  355. // AddTeamMember api for add a member to a team
  356. func AddTeamMember(ctx *context.APIContext) {
  357. // swagger:operation PUT /teams/{id}/members/{username} organization orgAddTeamMember
  358. // ---
  359. // summary: Add a team member
  360. // produces:
  361. // - application/json
  362. // parameters:
  363. // - name: id
  364. // in: path
  365. // description: id of the team
  366. // type: integer
  367. // format: int64
  368. // required: true
  369. // - name: username
  370. // in: path
  371. // description: username of the user to add
  372. // type: string
  373. // required: true
  374. // responses:
  375. // "204":
  376. // "$ref": "#/responses/empty"
  377. // "404":
  378. // "$ref": "#/responses/notFound"
  379. u := user.GetUserByParams(ctx)
  380. if ctx.Written() {
  381. return
  382. }
  383. if err := ctx.Org.Team.AddMember(u.ID); err != nil {
  384. ctx.Error(http.StatusInternalServerError, "AddMember", err)
  385. return
  386. }
  387. ctx.Status(http.StatusNoContent)
  388. }
  389. // RemoveTeamMember api for remove one member from a team
  390. func RemoveTeamMember(ctx *context.APIContext) {
  391. // swagger:operation DELETE /teams/{id}/members/{username} organization orgRemoveTeamMember
  392. // ---
  393. // summary: Remove a team member
  394. // produces:
  395. // - application/json
  396. // parameters:
  397. // - name: id
  398. // in: path
  399. // description: id of the team
  400. // type: integer
  401. // format: int64
  402. // required: true
  403. // - name: username
  404. // in: path
  405. // description: username of the user to remove
  406. // type: string
  407. // required: true
  408. // responses:
  409. // "204":
  410. // "$ref": "#/responses/empty"
  411. // "404":
  412. // "$ref": "#/responses/notFound"
  413. u := user.GetUserByParams(ctx)
  414. if ctx.Written() {
  415. return
  416. }
  417. if err := ctx.Org.Team.RemoveMember(u.ID); err != nil {
  418. ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
  419. return
  420. }
  421. ctx.Status(http.StatusNoContent)
  422. }
  423. // GetTeamRepos api for get a team's repos
  424. func GetTeamRepos(ctx *context.APIContext) {
  425. // swagger:operation GET /teams/{id}/repos organization orgListTeamRepos
  426. // ---
  427. // summary: List a team's repos
  428. // produces:
  429. // - application/json
  430. // parameters:
  431. // - name: id
  432. // in: path
  433. // description: id of the team
  434. // type: integer
  435. // format: int64
  436. // required: true
  437. // - name: page
  438. // in: query
  439. // description: page number of results to return (1-based)
  440. // type: integer
  441. // - name: limit
  442. // in: query
  443. // description: page size of results
  444. // type: integer
  445. // responses:
  446. // "200":
  447. // "$ref": "#/responses/RepositoryList"
  448. team := ctx.Org.Team
  449. if err := team.GetRepositories(&models.SearchTeamOptions{
  450. ListOptions: utils.GetListOptions(ctx),
  451. }); err != nil {
  452. ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
  453. }
  454. repos := make([]*api.Repository, len(team.Repos))
  455. for i, repo := range team.Repos {
  456. access, err := models.AccessLevel(ctx.User, repo)
  457. if err != nil {
  458. ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
  459. return
  460. }
  461. repos[i] = convert.ToRepo(repo, access)
  462. }
  463. ctx.JSON(http.StatusOK, repos)
  464. }
  465. // getRepositoryByParams get repository by a team's organization ID and repo name
  466. func getRepositoryByParams(ctx *context.APIContext) *models.Repository {
  467. repo, err := models.GetRepositoryByName(ctx.Org.Team.OrgID, ctx.Params(":reponame"))
  468. if err != nil {
  469. if models.IsErrRepoNotExist(err) {
  470. ctx.NotFound()
  471. } else {
  472. ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err)
  473. }
  474. return nil
  475. }
  476. return repo
  477. }
  478. // AddTeamRepository api for adding a repository to a team
  479. func AddTeamRepository(ctx *context.APIContext) {
  480. // swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository
  481. // ---
  482. // summary: Add a repository to a team
  483. // produces:
  484. // - application/json
  485. // parameters:
  486. // - name: id
  487. // in: path
  488. // description: id of the team
  489. // type: integer
  490. // format: int64
  491. // required: true
  492. // - name: org
  493. // in: path
  494. // description: organization that owns the repo to add
  495. // type: string
  496. // required: true
  497. // - name: repo
  498. // in: path
  499. // description: name of the repo to add
  500. // type: string
  501. // required: true
  502. // responses:
  503. // "204":
  504. // "$ref": "#/responses/empty"
  505. // "403":
  506. // "$ref": "#/responses/forbidden"
  507. repo := getRepositoryByParams(ctx)
  508. if ctx.Written() {
  509. return
  510. }
  511. if access, err := models.AccessLevel(ctx.User, repo); err != nil {
  512. ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
  513. return
  514. } else if access < models.AccessModeAdmin {
  515. ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
  516. return
  517. }
  518. if err := ctx.Org.Team.AddRepository(repo); err != nil {
  519. ctx.Error(http.StatusInternalServerError, "AddRepository", err)
  520. return
  521. }
  522. ctx.Status(http.StatusNoContent)
  523. }
  524. // RemoveTeamRepository api for removing a repository from a team
  525. func RemoveTeamRepository(ctx *context.APIContext) {
  526. // swagger:operation DELETE /teams/{id}/repos/{org}/{repo} organization orgRemoveTeamRepository
  527. // ---
  528. // summary: Remove a repository from a team
  529. // description: This does not delete the repository, it only removes the
  530. // repository from the team.
  531. // produces:
  532. // - application/json
  533. // parameters:
  534. // - name: id
  535. // in: path
  536. // description: id of the team
  537. // type: integer
  538. // format: int64
  539. // required: true
  540. // - name: org
  541. // in: path
  542. // description: organization that owns the repo to remove
  543. // type: string
  544. // required: true
  545. // - name: repo
  546. // in: path
  547. // description: name of the repo to remove
  548. // type: string
  549. // required: true
  550. // responses:
  551. // "204":
  552. // "$ref": "#/responses/empty"
  553. // "403":
  554. // "$ref": "#/responses/forbidden"
  555. repo := getRepositoryByParams(ctx)
  556. if ctx.Written() {
  557. return
  558. }
  559. if access, err := models.AccessLevel(ctx.User, repo); err != nil {
  560. ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
  561. return
  562. } else if access < models.AccessModeAdmin {
  563. ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
  564. return
  565. }
  566. if err := ctx.Org.Team.RemoveRepository(repo.ID); err != nil {
  567. ctx.Error(http.StatusInternalServerError, "RemoveRepository", err)
  568. return
  569. }
  570. ctx.Status(http.StatusNoContent)
  571. }
  572. // SearchTeam api for searching teams
  573. func SearchTeam(ctx *context.APIContext) {
  574. // swagger:operation GET /orgs/{org}/teams/search organization teamSearch
  575. // ---
  576. // summary: Search for teams within an organization
  577. // produces:
  578. // - application/json
  579. // parameters:
  580. // - name: org
  581. // in: path
  582. // description: name of the organization
  583. // type: string
  584. // required: true
  585. // - name: q
  586. // in: query
  587. // description: keywords to search
  588. // type: string
  589. // - name: include_desc
  590. // in: query
  591. // description: include search within team description (defaults to true)
  592. // type: boolean
  593. // - name: page
  594. // in: query
  595. // description: page number of results to return (1-based)
  596. // type: integer
  597. // - name: limit
  598. // in: query
  599. // description: page size of results
  600. // type: integer
  601. // responses:
  602. // "200":
  603. // description: "SearchResults of a successful search"
  604. // schema:
  605. // type: object
  606. // properties:
  607. // ok:
  608. // type: boolean
  609. // data:
  610. // type: array
  611. // items:
  612. // "$ref": "#/definitions/Team"
  613. listOptions := utils.GetListOptions(ctx)
  614. opts := &models.SearchTeamOptions{
  615. UserID: ctx.User.ID,
  616. Keyword: ctx.FormTrim("q"),
  617. OrgID: ctx.Org.Organization.ID,
  618. IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
  619. ListOptions: listOptions,
  620. }
  621. teams, maxResults, err := models.SearchTeam(opts)
  622. if err != nil {
  623. log.Error("SearchTeam failed: %v", err)
  624. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  625. "ok": false,
  626. "error": "SearchTeam internal failure",
  627. })
  628. return
  629. }
  630. apiTeams := make([]*api.Team, len(teams))
  631. for i := range teams {
  632. if err := teams[i].GetUnits(); err != nil {
  633. log.Error("Team GetUnits failed: %v", err)
  634. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  635. "ok": false,
  636. "error": "SearchTeam failed to get units",
  637. })
  638. return
  639. }
  640. apiTeams[i] = convert.ToTeam(teams[i])
  641. }
  642. ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
  643. ctx.SetTotalCountHeader(maxResults)
  644. ctx.JSON(http.StatusOK, map[string]interface{}{
  645. "ok": true,
  646. "data": apiTeams,
  647. })
  648. }