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 16KB

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