0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-11 09:15:31 +02:00

feat: add api routes to add/update/remove teams to/from repo groups

This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2026-05-05 20:52:42 -04:00
parent d92dc9e79b
commit 7b47ece487
No known key found for this signature in database
GPG Key ID: 924A5F6AF051E87C
3 changed files with 359 additions and 1 deletions

View File

@ -0,0 +1,13 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// CreateOrUpdateRepoGroupTeamOption options for adding a team to a repo group
type CreateOrUpdateRepoGroupTeamOption struct {
// Whether the team can create repositories and subgroups in the group
CanCreateIn *bool `json:"can_create_in"`
// example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"}
UnitsMap map[string]string `json:"units_map"`
Permission *RepoWritePermission `json:"permission"`
}

View File

@ -518,6 +518,25 @@ func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *co
ctx.APIErrorInternal(err)
return
}
isOrgOwner := false
isOrgAdmin := false
if ctx.Doer != nil {
isOrgOwner, err = organization.IsOrganizationOwner(ctx, g.OwnerID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
isOrgAdmin, err = organization.IsOrganizationAdmin(ctx, g.OwnerID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if isOrgOwner || isOrgAdmin {
return
}
}
canAccess, err := g.CanAccessAtLevel(ctx, ctx.Doer, mode)
if err != nil {
ctx.APIErrorInternal(err)
@ -541,7 +560,7 @@ func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *co
return
}
}
if !canCreateIn {
if !(canCreateIn || isOrgOwner || isOrgAdmin) {
ctx.APIError(http.StatusForbidden, fmt.Sprintf("User[%d] does not have permission to create new items in group[%d]", ctx.Doer.ID, gid))
return
}
@ -1841,6 +1860,14 @@ func Routes() *web.Router {
m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), bind(api.NewGroupOption{}), group.NewSubGroup)
m.Get("/subgroups", reqGroupMembership(perm.AccessModeRead, false), group.GetGroupSubGroups)
m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqGroupMembership(perm.AccessModeRead, false), group.GetGroupRepos)
m.Group("/teams", func() {
m.Get("", group.ListTeams)
m.Combo("/{team}").
Get(group.IsTeam).
Put(group.AddTeam, reqGroupMembership(perm.AccessModeAdmin, false)).
Patch(group.EditTeam, reqGroupMembership(perm.AccessModeAdmin, false)).
Delete(group.DeleteTeam, reqGroupMembership(perm.AccessModeAdmin, false))
}, reqToken(), reqGroupMembership(perm.AccessModeRead, false))
}, checkTokenPublicOnly())
})
return m

View File

@ -0,0 +1,318 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package group
import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/db"
group_model "code.gitea.io/gitea/models/group"
org_model "code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
org_group_model "code.gitea.io/gitea/models/shared/group"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
group_service "code.gitea.io/gitea/services/group"
)
// ListTeams list a repository group's teams
func ListTeams(ctx *context.APIContext) {
// swagger:operation GET /groups/{group_id}/teams repository-group repoGroupListTeams
// ---
// summary: List a repository group's teams
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/TeamList"
// "404":
// "$ref": "#/responses/notFound"
var (
err error
group *group_model.Group
)
group, err = group_model.GetGroupByID(ctx, ctx.PathParamInt64("group_id"))
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
teams, err := org_group_model.GetGroupTeams(ctx, group.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiTeams, err := convert.ToTeams(ctx, teams, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeams)
}
// AddTeam add a team to a repository group
func AddTeam(ctx *context.APIContext) {
// swagger:operation PUT /groups/{group_id}/teams/{team} repository-group repoGroupAddTeam
// ---
// summary: Add a team to a repository group
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateRepoGroupTeamOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.CreateOrUpdateRepoGroupTeamOption)
changeGroupTeam(ctx, form, true)
}
// EditTeam update a team assigned to a repository group
func EditTeam(ctx *context.APIContext) {
// swagger:operation PATCH /groups/{group_id}/teams/{team} repository-group repoGroupEditTeam
// ---
// summary: Update a team assigned to a repository group
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateRepoGroupTeamOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.CreateOrUpdateRepoGroupTeamOption)
gid := ctx.PathParamInt64("group_id")
group, err := group_model.GetGroupByID(ctx, gid)
if err != nil {
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
}
ctx.APIErrorInternal(err)
return
}
team := getTeamFromGroup(ctx, group)
gt, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, team.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if gt == nil {
ctx.APIErrorNotFound()
return
}
if form.CanCreateIn != nil {
gt.CanCreateIn = *form.CanCreateIn
}
if form.Permission != nil {
gt.AccessMode = perm_model.ParseAccessMode(string(*form.Permission))
}
err = group_service.UpdateGroupTeam(ctx, gt, form.UnitsMap)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteTeam delete a team from a repository group
func DeleteTeam(ctx *context.APIContext) {
// swagger:operation DELETE /groups/{group_id}/teams/{team} repository-group repoGroupDeleteTeam
// ---
// summary: Add a team to a repository group
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
changeGroupTeam(ctx, nil, false)
}
// IsTeam check if a team is assigned to a repository
func IsTeam(ctx *context.APIContext) {
// swagger:operation GET /groups/{group_id}/teams/{team} repository-group repoGroupCheckTeam
// ---
// summary: Check if a team is assigned to a repository group
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Team"
// "404":
// "$ref": "#/responses/notFound"
gid := ctx.PathParamInt64("group_id")
group, err := group_model.GetGroupByID(ctx, gid)
if err != nil {
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
team := getTeamFromGroup(ctx, group)
if team == nil {
return
}
if group_model.HasTeamGroup(ctx, group.OwnerID, team.ID, gid) {
apiTeam, err := convert.ToTeam(ctx, team)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeam)
return
}
ctx.APIErrorNotFound()
}
func getTeamFromGroup(ctx *context.APIContext, group *group_model.Group) *org_model.Team {
teamName := ctx.PathParam("team")
team, err := org_model.GetTeam(ctx, group.OwnerID, teamName)
if err != nil {
if org_model.IsErrTeamNotExist(err) {
ctx.APIErrorNotFound()
return nil
}
ctx.APIErrorInternal(err)
return nil
}
return team
}
func changeGroupTeam(ctx *context.APIContext, options *api.CreateOrUpdateRepoGroupTeamOption, add bool) {
gid := ctx.PathParamInt64("group_id")
group, err := group_model.GetGroupByID(ctx, gid)
if err != nil {
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
err = group.LoadOwner(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
team := getTeamFromGroup(ctx, group)
if team == nil {
return
}
groupHasTeam := group_model.HasTeamGroup(ctx, group.OwnerID, team.ID, gid)
if add {
if groupHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to group", team.Name))
return
}
var accessModeArg *perm_model.AccessMode
if options.Permission != nil {
accessModeArg = new(perm_model.ParseAccessMode(string(*options.Permission)))
}
err = group_service.AddTeamToGroup(ctx, group, team.Name, options.UnitsMap, options.CanCreateIn, accessModeArg)
if err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
err = group_service.DeleteTeamFromGroup(ctx, group, group.OwnerID, team.Name)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if _, err = db.GetEngine(ctx).Where("group_id = ?", gid).Delete(new(group_model.RepoGroupUnit)); err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.Status(http.StatusNoContent)
}