mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
Gitea replaces a team's whole unit policy on edit, so `tea team edit 5 --unit repo.wiki=write` revoked code, issue and pull request access. team edit now merges --unit into the current policy; --unit-replace keeps the old replacing behavior. Signed-off-by: bircni <bircni@icloud.com>
187 lines
7.1 KiB
Go
187 lines
7.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"strings"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
"gitea.dev/tea/internal/transport"
|
|
)
|
|
|
|
// Team is an organization's access group and its permission policy.
|
|
type Team struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Organization string `json:"organization"`
|
|
Permission string `json:"permission"`
|
|
CanCreateRepo bool `json:"can_create_repo"`
|
|
AllRepositories bool `json:"all_repositories"`
|
|
Units map[string]string `json:"units"`
|
|
}
|
|
|
|
func teamFrom(team *gitea.Team) Team {
|
|
result := Team{
|
|
ID: team.ID, Name: team.Name, Description: team.Description, Permission: string(team.Permission),
|
|
CanCreateRepo: team.CanCreateOrgRepo, AllRepositories: team.IncludesAllRepositories, Units: team.UnitsMap,
|
|
}
|
|
if result.Units == nil {
|
|
result.Units = map[string]string{}
|
|
}
|
|
if team.Organization != nil {
|
|
result.Organization = organizationFrom(team.Organization).Name
|
|
}
|
|
return result
|
|
}
|
|
|
|
// TeamOptions preserves omitted fields when changing a team.
|
|
type TeamOptions struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Permission *string `json:"permission,omitempty"`
|
|
CanCreateRepo *bool `json:"can_create_org_repo,omitempty"`
|
|
AllRepositories *bool `json:"includes_all_repositories,omitempty"`
|
|
Units map[string]string `json:"units_map,omitempty"`
|
|
// ReplaceUnits makes Edit replace the unit policy with Units instead of merging Units into it.
|
|
ReplaceUnits bool `json:"-"`
|
|
}
|
|
|
|
// Teams implements team, member and repository access operations.
|
|
type Teams struct{ Client *transport.Client }
|
|
|
|
// List lists teams in one organization.
|
|
func (s Teams) List(ctx context.Context, org string, limit, page int) ([]Team, error) {
|
|
teams, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Team, *gitea.Response, error) {
|
|
return c.Organizations.ListOrgTeams(ctx, org, gitea.ListTeamsOptions{ListOptions: listOptions(limit, page)})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAll(teams, teamFrom), nil
|
|
}
|
|
|
|
// ByName indexes every team of an organization by lower-case name, which Gitea keeps unique.
|
|
func (s Teams) ByName(ctx context.Context, org string) (map[string]Team, error) {
|
|
teams, err := listAll(func(page int) ([]Team, error) { return s.List(ctx, org, 50, page) })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string]Team, len(teams))
|
|
for _, team := range teams {
|
|
result[strings.ToLower(team.Name)] = team
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// View retrieves a team and its current permissions.
|
|
func (s Teams) View(ctx context.Context, id int64) (Team, error) {
|
|
team, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Team, *gitea.Response, error) {
|
|
return c.Organizations.GetTeam(ctx, id)
|
|
})
|
|
if err != nil {
|
|
return Team{}, err
|
|
}
|
|
return teamFrom(team), nil
|
|
}
|
|
|
|
// Create creates an organization team with explicit initial permissions.
|
|
func (s Teams) Create(ctx context.Context, org string, opts TeamOptions) (Team, error) {
|
|
team, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Team, *gitea.Response, error) {
|
|
return c.Organizations.CreateTeam(ctx, org, gitea.CreateTeamOption{
|
|
Name: deref(opts.Name), Description: deref(opts.Description), Permission: gitea.AccessMode(deref(opts.Permission)),
|
|
CanCreateOrgRepo: deref(opts.CanCreateRepo), IncludesAllRepositories: deref(opts.AllRepositories), UnitsMap: opts.Units,
|
|
})
|
|
})
|
|
if err != nil {
|
|
return Team{}, err
|
|
}
|
|
return teamFrom(team), nil
|
|
}
|
|
|
|
// Edit merges Units into the current unit policy unless ReplaceUnits is set, and preserves unit
|
|
// permissions when changing access to all repositories. Gitea replaces the whole policy on edit.
|
|
func (s Teams) Edit(ctx context.Context, id int64, opts TeamOptions) (Team, error) {
|
|
repositoryPolicy := opts.AllRepositories != nil && opts.Permission == nil
|
|
if repositoryPolicy || (opts.Units != nil && !opts.ReplaceUnits) {
|
|
current, err := s.View(ctx, id)
|
|
if err != nil {
|
|
return Team{}, err
|
|
}
|
|
if repositoryPolicy {
|
|
if current.Permission == "owner" {
|
|
return Team{}, &CapabilityError{Feature: "owner-team repository policy editing", Reason: "owner teams already manage all organization repositories"}
|
|
}
|
|
opts.Permission = ¤t.Permission
|
|
}
|
|
switch {
|
|
case opts.Units == nil:
|
|
opts.Units = current.Units
|
|
case !opts.ReplaceUnits:
|
|
units := maps.Clone(current.Units)
|
|
maps.Copy(units, opts.Units)
|
|
opts.Units = units
|
|
}
|
|
}
|
|
var team gitea.Team
|
|
// TODO(sdk): EditTeamOption requires a name of at most 30 characters and a permission, and EditTeam should return the team.
|
|
if err := sendJSON(ctx, s.Client, http.MethodPatch, fmt.Sprintf("teams/%d", id), opts, &team); err != nil {
|
|
return Team{}, err
|
|
}
|
|
return teamFrom(&team), nil
|
|
}
|
|
|
|
// Delete deletes a team and its associated access grants.
|
|
func (s Teams) Delete(ctx context.Context, id int64) error {
|
|
return do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
return c.Organizations.DeleteTeam(ctx, id)
|
|
})
|
|
}
|
|
|
|
// Members lists members of a team.
|
|
func (s Teams) Members(ctx context.Context, id int64, limit, page int) ([]OrganizationMember, error) {
|
|
users, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.User, *gitea.Response, error) {
|
|
return c.Organizations.ListTeamMembers(ctx, id, gitea.ListTeamMembersOptions{ListOptions: listOptions(limit, page)})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAll(users, memberFrom), nil
|
|
}
|
|
|
|
// SetMember adds or removes one team membership.
|
|
func (s Teams) SetMember(ctx context.Context, id int64, username string, add bool) error {
|
|
return do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
if add {
|
|
return c.Organizations.AddTeamMember(ctx, id, username)
|
|
}
|
|
return c.Organizations.RemoveTeamMember(ctx, id, username)
|
|
})
|
|
}
|
|
|
|
// Repositories lists repositories accessible to a team.
|
|
func (s Teams) Repositories(ctx context.Context, id int64, limit, page int) ([]Repository, error) {
|
|
repos, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
|
return c.Organizations.ListTeamRepositories(ctx, id, gitea.ListTeamRepositoriesOptions{ListOptions: listOptions(limit, page)})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAll(repos, repositoryFrom), nil
|
|
}
|
|
|
|
// SetRepository adds or removes team access to a repository.
|
|
func (s Teams) SetRepository(ctx context.Context, id int64, repository string, add bool) error {
|
|
return repoDo(ctx, s.Client, repository, func(ctx context.Context, c *gitea.Client, owner, name string) (*gitea.Response, error) {
|
|
if add {
|
|
return c.Organizations.AddTeamRepository(ctx, id, owner, name)
|
|
}
|
|
return c.Organizations.RemoveTeamRepository(ctx, id, owner, name)
|
|
})
|
|
}
|