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.

650 lines
16 KiB

  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. if err := team.GetUnits(); err != nil {
  176. ctx.InternalServerError(err)
  177. return
  178. }
  179. if form.CanCreateOrgRepo != nil {
  180. team.CanCreateOrgRepo = *form.CanCreateOrgRepo
  181. }
  182. if len(form.Name) > 0 {
  183. team.Name = form.Name
  184. }
  185. if form.Description != nil {
  186. team.Description = *form.Description
  187. }
  188. isAuthChanged := false
  189. isIncludeAllChanged := false
  190. if !team.IsOwnerTeam() && len(form.Permission) != 0 {
  191. // Validate permission level.
  192. auth := models.ParseAccessMode(form.Permission)
  193. if team.Authorize != auth {
  194. isAuthChanged = true
  195. team.Authorize = auth
  196. }
  197. if form.IncludesAllRepositories != nil {
  198. isIncludeAllChanged = true
  199. team.IncludesAllRepositories = *form.IncludesAllRepositories
  200. }
  201. }
  202. if team.Authorize < models.AccessModeOwner {
  203. if len(form.Units) > 0 {
  204. var units = make([]*models.TeamUnit, 0, len(form.Units))
  205. unitTypes := models.FindUnitTypes(form.Units...)
  206. for _, tp := range unitTypes {
  207. units = append(units, &models.TeamUnit{
  208. OrgID: ctx.Org.Team.OrgID,
  209. Type: tp,
  210. })
  211. }
  212. team.Units = units
  213. }
  214. }
  215. if err := models.UpdateTeam(team, isAuthChanged, isIncludeAllChanged); err != nil {
  216. ctx.Error(http.StatusInternalServerError, "EditTeam", err)
  217. return
  218. }
  219. ctx.JSON(http.StatusOK, convert.ToTeam(team))
  220. }
  221. // DeleteTeam api for delete a team
  222. func DeleteTeam(ctx *context.APIContext) {
  223. // swagger:operation DELETE /teams/{id} organization orgDeleteTeam
  224. // ---
  225. // summary: Delete a team
  226. // parameters:
  227. // - name: id
  228. // in: path
  229. // description: id of the team to delete
  230. // type: integer
  231. // format: int64
  232. // required: true
  233. // responses:
  234. // "204":
  235. // description: team deleted
  236. if err := models.DeleteTeam(ctx.Org.Team); err != nil {
  237. ctx.Error(http.StatusInternalServerError, "DeleteTeam", err)
  238. return
  239. }
  240. ctx.Status(http.StatusNoContent)
  241. }
  242. // GetTeamMembers api for get a team's members
  243. func GetTeamMembers(ctx *context.APIContext) {
  244. // swagger:operation GET /teams/{id}/members organization orgListTeamMembers
  245. // ---
  246. // summary: List a team's members
  247. // produces:
  248. // - application/json
  249. // parameters:
  250. // - name: id
  251. // in: path
  252. // description: id of the team
  253. // type: integer
  254. // format: int64
  255. // required: true
  256. // responses:
  257. // "200":
  258. // "$ref": "#/responses/UserList"
  259. isMember, err := models.IsOrganizationMember(ctx.Org.Team.OrgID, ctx.User.ID)
  260. if err != nil {
  261. ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
  262. return
  263. } else if !isMember {
  264. ctx.NotFound()
  265. return
  266. }
  267. team := ctx.Org.Team
  268. if err := team.GetMembers(); err != nil {
  269. ctx.Error(http.StatusInternalServerError, "GetTeamMembers", err)
  270. return
  271. }
  272. members := make([]*api.User, len(team.Members))
  273. for i, member := range team.Members {
  274. members[i] = convert.ToUser(member, ctx.IsSigned, ctx.User.IsAdmin)
  275. }
  276. ctx.JSON(http.StatusOK, members)
  277. }
  278. // GetTeamMember api for get a particular member of team
  279. func GetTeamMember(ctx *context.APIContext) {
  280. // swagger:operation GET /teams/{id}/members/{username} organization orgListTeamMember
  281. // ---
  282. // summary: List a particular member of team
  283. // produces:
  284. // - application/json
  285. // parameters:
  286. // - name: id
  287. // in: path
  288. // description: id of the team
  289. // type: integer
  290. // format: int64
  291. // required: true
  292. // - name: username
  293. // in: path
  294. // description: username of the member to list
  295. // type: string
  296. // required: true
  297. // responses:
  298. // "200":
  299. // "$ref": "#/responses/User"
  300. // "404":
  301. // "$ref": "#/responses/notFound"
  302. u := user.GetUserByParams(ctx)
  303. if ctx.Written() {
  304. return
  305. }
  306. teamID := ctx.ParamsInt64("teamid")
  307. isTeamMember, err := models.IsUserInTeams(u.ID, []int64{teamID})
  308. if err != nil {
  309. ctx.Error(http.StatusInternalServerError, "IsUserInTeams", err)
  310. return
  311. } else if !isTeamMember {
  312. ctx.NotFound()
  313. return
  314. }
  315. ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.IsSigned, ctx.User.IsAdmin))
  316. }
  317. // AddTeamMember api for add a member to a team
  318. func AddTeamMember(ctx *context.APIContext) {
  319. // swagger:operation PUT /teams/{id}/members/{username} organization orgAddTeamMember
  320. // ---
  321. // summary: Add a team member
  322. // produces:
  323. // - application/json
  324. // parameters:
  325. // - name: id
  326. // in: path
  327. // description: id of the team
  328. // type: integer
  329. // format: int64
  330. // required: true
  331. // - name: username
  332. // in: path
  333. // description: username of the user to add
  334. // type: string
  335. // required: true
  336. // responses:
  337. // "204":
  338. // "$ref": "#/responses/empty"
  339. // "404":
  340. // "$ref": "#/responses/notFound"
  341. u := user.GetUserByParams(ctx)
  342. if ctx.Written() {
  343. return
  344. }
  345. if err := ctx.Org.Team.AddMember(u.ID); err != nil {
  346. ctx.Error(http.StatusInternalServerError, "AddMember", err)
  347. return
  348. }
  349. ctx.Status(http.StatusNoContent)
  350. }
  351. // RemoveTeamMember api for remove one member from a team
  352. func RemoveTeamMember(ctx *context.APIContext) {
  353. // swagger:operation DELETE /teams/{id}/members/{username} organization orgRemoveTeamMember
  354. // ---
  355. // summary: Remove a team member
  356. // produces:
  357. // - application/json
  358. // parameters:
  359. // - name: id
  360. // in: path
  361. // description: id of the team
  362. // type: integer
  363. // format: int64
  364. // required: true
  365. // - name: username
  366. // in: path
  367. // description: username of the user to remove
  368. // type: string
  369. // required: true
  370. // responses:
  371. // "204":
  372. // "$ref": "#/responses/empty"
  373. // "404":
  374. // "$ref": "#/responses/notFound"
  375. u := user.GetUserByParams(ctx)
  376. if ctx.Written() {
  377. return
  378. }
  379. if err := ctx.Org.Team.RemoveMember(u.ID); err != nil {
  380. ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
  381. return
  382. }
  383. ctx.Status(http.StatusNoContent)
  384. }
  385. // GetTeamRepos api for get a team's repos
  386. func GetTeamRepos(ctx *context.APIContext) {
  387. // swagger:operation GET /teams/{id}/repos organization orgListTeamRepos
  388. // ---
  389. // summary: List a team's repos
  390. // produces:
  391. // - application/json
  392. // parameters:
  393. // - name: id
  394. // in: path
  395. // description: id of the team
  396. // type: integer
  397. // format: int64
  398. // required: true
  399. // responses:
  400. // "200":
  401. // "$ref": "#/responses/RepositoryList"
  402. team := ctx.Org.Team
  403. if err := team.GetRepositories(); err != nil {
  404. ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
  405. }
  406. repos := make([]*api.Repository, len(team.Repos))
  407. for i, repo := range team.Repos {
  408. access, err := models.AccessLevel(ctx.User, repo)
  409. if err != nil {
  410. ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
  411. return
  412. }
  413. repos[i] = repo.APIFormat(access)
  414. }
  415. ctx.JSON(http.StatusOK, repos)
  416. }
  417. // getRepositoryByParams get repository by a team's organization ID and repo name
  418. func getRepositoryByParams(ctx *context.APIContext) *models.Repository {
  419. repo, err := models.GetRepositoryByName(ctx.Org.Team.OrgID, ctx.Params(":reponame"))
  420. if err != nil {
  421. if models.IsErrRepoNotExist(err) {
  422. ctx.NotFound()
  423. } else {
  424. ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err)
  425. }
  426. return nil
  427. }
  428. return repo
  429. }
  430. // AddTeamRepository api for adding a repository to a team
  431. func AddTeamRepository(ctx *context.APIContext) {
  432. // swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository
  433. // ---
  434. // summary: Add a repository to a team
  435. // produces:
  436. // - application/json
  437. // parameters:
  438. // - name: id
  439. // in: path
  440. // description: id of the team
  441. // type: integer
  442. // format: int64
  443. // required: true
  444. // - name: org
  445. // in: path
  446. // description: organization that owns the repo to add
  447. // type: string
  448. // required: true
  449. // - name: repo
  450. // in: path
  451. // description: name of the repo to add
  452. // type: string
  453. // required: true
  454. // responses:
  455. // "204":
  456. // "$ref": "#/responses/empty"
  457. // "403":
  458. // "$ref": "#/responses/forbidden"
  459. repo := getRepositoryByParams(ctx)
  460. if ctx.Written() {
  461. return
  462. }
  463. if access, err := models.AccessLevel(ctx.User, repo); err != nil {
  464. ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
  465. return
  466. } else if access < models.AccessModeAdmin {
  467. ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
  468. return
  469. }
  470. if err := ctx.Org.Team.AddRepository(repo); err != nil {
  471. ctx.Error(http.StatusInternalServerError, "AddRepository", err)
  472. return
  473. }
  474. ctx.Status(http.StatusNoContent)
  475. }
  476. // RemoveTeamRepository api for removing a repository from a team
  477. func RemoveTeamRepository(ctx *context.APIContext) {
  478. // swagger:operation DELETE /teams/{id}/repos/{org}/{repo} organization orgRemoveTeamRepository
  479. // ---
  480. // summary: Remove a repository from a team
  481. // description: This does not delete the repository, it only removes the
  482. // repository from the 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 remove
  495. // type: string
  496. // required: true
  497. // - name: repo
  498. // in: path
  499. // description: name of the repo to remove
  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.RemoveRepository(repo.ID); err != nil {
  519. ctx.Error(http.StatusInternalServerError, "RemoveRepository", err)
  520. return
  521. }
  522. ctx.Status(http.StatusNoContent)
  523. }
  524. // SearchTeam api for searching teams
  525. func SearchTeam(ctx *context.APIContext) {
  526. // swagger:operation GET /orgs/{org}/teams/search organization teamSearch
  527. // ---
  528. // summary: Search for teams within an organization
  529. // produces:
  530. // - application/json
  531. // parameters:
  532. // - name: org
  533. // in: path
  534. // description: name of the organization
  535. // type: string
  536. // required: true
  537. // - name: q
  538. // in: query
  539. // description: keywords to search
  540. // type: string
  541. // - name: include_desc
  542. // in: query
  543. // description: include search within team description (defaults to true)
  544. // type: boolean
  545. // - name: limit
  546. // in: query
  547. // description: limit size of results
  548. // type: integer
  549. // - name: page
  550. // in: query
  551. // description: page number of results to return (1-based)
  552. // type: integer
  553. // responses:
  554. // "200":
  555. // description: "SearchResults of a successful search"
  556. // schema:
  557. // type: object
  558. // properties:
  559. // ok:
  560. // type: boolean
  561. // data:
  562. // type: array
  563. // items:
  564. // "$ref": "#/definitions/Team"
  565. opts := &models.SearchTeamOptions{
  566. UserID: ctx.User.ID,
  567. Keyword: strings.TrimSpace(ctx.Query("q")),
  568. OrgID: ctx.Org.Organization.ID,
  569. IncludeDesc: (ctx.Query("include_desc") == "" || ctx.QueryBool("include_desc")),
  570. PageSize: ctx.QueryInt("limit"),
  571. Page: ctx.QueryInt("page"),
  572. }
  573. teams, _, err := models.SearchTeam(opts)
  574. if err != nil {
  575. log.Error("SearchTeam failed: %v", err)
  576. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  577. "ok": false,
  578. "error": "SearchTeam internal failure",
  579. })
  580. return
  581. }
  582. apiTeams := make([]*api.Team, len(teams))
  583. for i := range teams {
  584. if err := teams[i].GetUnits(); err != nil {
  585. log.Error("Team GetUnits failed: %v", err)
  586. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  587. "ok": false,
  588. "error": "SearchTeam failed to get units",
  589. })
  590. return
  591. }
  592. apiTeams[i] = convert.ToTeam(teams[i])
  593. }
  594. ctx.JSON(http.StatusOK, map[string]interface{}{
  595. "ok": true,
  596. "data": apiTeams,
  597. })
  598. }