* Add field IsAllRepositories to team
* Add AllRepositories to team UI
* Manage team with access to all repositories
* Add field IsAllRepositories to team API
* put backticks around table/column names
* rename IsAllRepositories to IncludesAllRepositories
* do not reload slice if already loaded
* add repo to teams with access to all repositories when changing repo owner
* improve tests for teams with access to all repositories
* Merge branch 'master'
* Change code for adding all repositories
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* fmt after merge
* Change code in API EditTeam similar to EditTeamPost web interface
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* Clarify that all repositories will be added
Signed-off-by: David Svantesson <davidsvantesson@gmail.com>
* All repositories option under Permissions headline
* New setting group 'Repository access'
* Move check IncludeAllRepositories to removeRepository.
* Revert "Move check IncludeAllRepositories to removeRepository." and add comment instead.
This reverts commit 753b7d205b
.
* Clarify help text what options do.
tags/v1.11.0-rc1
@@ -55,37 +55,44 @@ func TestAPITeam(t *testing.T) { | |||
// Create team. | |||
teamToCreate := &api.CreateTeamOption{ | |||
Name: "team1", | |||
Description: "team one", | |||
Permission: "write", | |||
Units: []string{"repo.code", "repo.issues"}, | |||
Name: "team1", | |||
Description: "team one", | |||
IncludesAllRepositories: true, | |||
Permission: "write", | |||
Units: []string{"repo.code", "repo.issues"}, | |||
} | |||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate) | |||
resp = session.MakeRequest(t, req, http.StatusCreated) | |||
DecodeJSON(t, resp, &apiTeam) | |||
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units) | |||
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.Permission, teamToCreate.Units) | |||
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, | |||
teamToCreate.Permission, teamToCreate.Units) | |||
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, | |||
teamToCreate.Permission, teamToCreate.Units) | |||
teamID := apiTeam.ID | |||
// Edit team. | |||
teamToEdit := &api.EditTeamOption{ | |||
Name: "teamone", | |||
Description: "team 1", | |||
Permission: "admin", | |||
Units: []string{"repo.code", "repo.pulls", "repo.releases"}, | |||
Name: "teamone", | |||
Description: "team 1", | |||
IncludesAllRepositories: false, | |||
Permission: "admin", | |||
Units: []string{"repo.code", "repo.pulls", "repo.releases"}, | |||
} | |||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit) | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
DecodeJSON(t, resp, &apiTeam) | |||
checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units) | |||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.Permission, teamToEdit.Units) | |||
checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, | |||
teamToEdit.Permission, teamToEdit.Units) | |||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, | |||
teamToEdit.Permission, teamToEdit.Units) | |||
// Read team. | |||
teamRead := models.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team) | |||
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID) | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
DecodeJSON(t, resp, &apiTeam) | |||
checkTeamResponse(t, &apiTeam, teamRead.Name, teamRead.Description, teamRead.Authorize.String(), teamRead.GetUnitNames()) | |||
checkTeamResponse(t, &apiTeam, teamRead.Name, teamRead.Description, teamRead.IncludesAllRepositories, | |||
teamRead.Authorize.String(), teamRead.GetUnitNames()) | |||
// Delete team. | |||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID) | |||
@@ -93,19 +100,20 @@ func TestAPITeam(t *testing.T) { | |||
models.AssertNotExistsBean(t, &models.Team{ID: teamID}) | |||
} | |||
func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, permission string, units []string) { | |||
func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string) { | |||
assert.Equal(t, name, apiTeam.Name, "name") | |||
assert.Equal(t, description, apiTeam.Description, "description") | |||
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories") | |||
assert.Equal(t, permission, apiTeam.Permission, "permission") | |||
sort.StringSlice(units).Sort() | |||
sort.StringSlice(apiTeam.Units).Sort() | |||
assert.EqualValues(t, units, apiTeam.Units, "units") | |||
} | |||
func checkTeamBean(t *testing.T, id int64, name, description string, permission string, units []string) { | |||
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string) { | |||
team := models.AssertExistsAndLoadBean(t, &models.Team{ID: id}).(*models.Team) | |||
assert.NoError(t, team.GetUnits(), "GetUnits") | |||
checkTeamResponse(t, convert.ToTeam(team), name, description, permission, units) | |||
checkTeamResponse(t, convert.ToTeam(team), name, description, includesAllRepositories, permission, units) | |||
} | |||
type TeamSearchResults struct { |
@@ -264,6 +264,8 @@ var migrations = []Migration{ | |||
NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches), | |||
// v104 -> v105 | |||
NewMigration("remove unnecessary columns from label", removeLabelUneededCols), | |||
// v105 -> v106 | |||
NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories), | |||
} | |||
// Migrate database to current version |
@@ -0,0 +1,25 @@ | |||
// Copyright 2019 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package migrations | |||
import ( | |||
"xorm.io/xorm" | |||
) | |||
func addTeamIncludesAllRepositories(x *xorm.Engine) error { | |||
type Team struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` | |||
} | |||
if err := x.Sync2(new(Team)); err != nil { | |||
return err | |||
} | |||
_, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?", | |||
true, "Owners") | |||
return err | |||
} |
@@ -48,6 +48,9 @@ func (org *User) GetOwnerTeam() (*Team, error) { | |||
} | |||
func (org *User) getTeams(e Engine) error { | |||
if org.Teams != nil { | |||
return nil | |||
} | |||
return e. | |||
Where("org_id=?", org.ID). | |||
OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END"). | |||
@@ -149,11 +152,12 @@ func CreateOrganization(org, owner *User) (err error) { | |||
// Create default owner team. | |||
t := &Team{ | |||
OrgID: org.ID, | |||
LowerName: strings.ToLower(ownerTeamName), | |||
Name: ownerTeamName, | |||
Authorize: AccessModeOwner, | |||
NumMembers: 1, | |||
OrgID: org.ID, | |||
LowerName: strings.ToLower(ownerTeamName), | |||
Name: ownerTeamName, | |||
Authorize: AccessModeOwner, | |||
NumMembers: 1, | |||
IncludesAllRepositories: true, | |||
} | |||
if _, err = sess.Insert(t); err != nil { | |||
return fmt.Errorf("insert owner team: %v", err) |
@@ -22,17 +22,18 @@ const ownerTeamName = "Owners" | |||
// Team represents a organization team. | |||
type Team struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
OrgID int64 `xorm:"INDEX"` | |||
LowerName string | |||
Name string | |||
Description string | |||
Authorize AccessMode | |||
Repos []*Repository `xorm:"-"` | |||
Members []*User `xorm:"-"` | |||
NumRepos int | |||
NumMembers int | |||
Units []*TeamUnit `xorm:"-"` | |||
ID int64 `xorm:"pk autoincr"` | |||
OrgID int64 `xorm:"INDEX"` | |||
LowerName string | |||
Name string | |||
Description string | |||
Authorize AccessMode | |||
Repos []*Repository `xorm:"-"` | |||
Members []*User `xorm:"-"` | |||
NumRepos int | |||
NumMembers int | |||
Units []*TeamUnit `xorm:"-"` | |||
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` | |||
} | |||
// SearchTeamOptions holds the search options | |||
@@ -149,6 +150,9 @@ func (t *Team) IsMember(userID int64) bool { | |||
} | |||
func (t *Team) getRepositories(e Engine) error { | |||
if t.Repos != nil { | |||
return nil | |||
} | |||
return e.Join("INNER", "team_repo", "repository.id = team_repo.repo_id"). | |||
Where("team_repo.team_id=?", t.ID). | |||
OrderBy("repository.name"). | |||
@@ -220,6 +224,25 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) { | |||
return nil | |||
} | |||
// addAllRepositories adds all repositories to the team. | |||
// If the team already has some repositories they will be left unchanged. | |||
func (t *Team) addAllRepositories(e Engine) error { | |||
var orgRepos []Repository | |||
if err := e.Where("owner_id = ?", t.OrgID).Find(&orgRepos); err != nil { | |||
return fmt.Errorf("get org repos: %v", err) | |||
} | |||
for _, repo := range orgRepos { | |||
if !t.hasRepository(e, repo.ID) { | |||
if err := t.addRepository(e, &repo); err != nil { | |||
return fmt.Errorf("addRepository: %v", err) | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
// AddRepository adds new repository to team of organization. | |||
func (t *Team) AddRepository(repo *Repository) (err error) { | |||
if repo.OwnerID != t.OrgID { | |||
@@ -241,6 +264,8 @@ func (t *Team) AddRepository(repo *Repository) (err error) { | |||
return sess.Commit() | |||
} | |||
// removeRepository removes a repository from a team and recalculates access | |||
// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted) | |||
func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) { | |||
if err = removeTeamRepo(e, t.ID, repo.ID); err != nil { | |||
return err | |||
@@ -284,11 +309,16 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e | |||
} | |||
// RemoveRepository removes repository from team of organization. | |||
// If the team shall include all repositories the request is ignored. | |||
func (t *Team) RemoveRepository(repoID int64) error { | |||
if !t.HasRepository(repoID) { | |||
return nil | |||
} | |||
if t.IncludesAllRepositories { | |||
return nil | |||
} | |||
repo, err := GetRepositoryByID(repoID) | |||
if err != nil { | |||
return err | |||
@@ -394,6 +424,14 @@ func NewTeam(t *Team) (err error) { | |||
} | |||
} | |||
// Add all repositories to the team if it has access to all of them. | |||
if t.IncludesAllRepositories { | |||
err = t.addAllRepositories(sess) | |||
if err != nil { | |||
return fmt.Errorf("addAllRepositories: %v", err) | |||
} | |||
} | |||
// Update organization number of teams. | |||
if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil { | |||
errRollback := sess.Rollback() | |||
@@ -446,7 +484,7 @@ func GetTeamByID(teamID int64) (*Team, error) { | |||
} | |||
// UpdateTeam updates information of team. | |||
func UpdateTeam(t *Team, authChanged bool) (err error) { | |||
func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) { | |||
if len(t.Name) == 0 { | |||
return errors.New("empty team name") | |||
} | |||
@@ -511,6 +549,14 @@ func UpdateTeam(t *Team, authChanged bool) (err error) { | |||
} | |||
} | |||
// Add all repositories to the team if it has access to all of them. | |||
if includeAllChanged && t.IncludesAllRepositories { | |||
err = t.addAllRepositories(sess) | |||
if err != nil { | |||
return fmt.Errorf("addAllRepositories: %v", err) | |||
} | |||
} | |||
return sess.Commit() | |||
} | |||
@@ -5,9 +5,12 @@ | |||
package models | |||
import ( | |||
"fmt" | |||
"strings" | |||
"testing" | |||
"code.gitea.io/gitea/modules/structs" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
@@ -206,7 +209,7 @@ func TestUpdateTeam(t *testing.T) { | |||
team.Name = "newName" | |||
team.Description = strings.Repeat("A long description!", 100) | |||
team.Authorize = AccessModeAdmin | |||
assert.NoError(t, UpdateTeam(team, true)) | |||
assert.NoError(t, UpdateTeam(team, true, false)) | |||
team = AssertExistsAndLoadBean(t, &Team{Name: "newName"}).(*Team) | |||
assert.True(t, strings.HasPrefix(team.Description, "A long description!")) | |||
@@ -225,7 +228,7 @@ func TestUpdateTeam2(t *testing.T) { | |||
team.LowerName = "owners" | |||
team.Name = "Owners" | |||
team.Description = strings.Repeat("A long description!", 100) | |||
err := UpdateTeam(team, true) | |||
err := UpdateTeam(team, true, false) | |||
assert.True(t, IsErrTeamAlreadyExist(err)) | |||
CheckConsistencyFor(t, &Team{ID: team.ID}) | |||
@@ -374,3 +377,133 @@ func TestUsersInTeamsCount(t *testing.T) { | |||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2) // userid 2,4 | |||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5 | |||
} | |||
func TestIncludesAllRepositoriesTeams(t *testing.T) { | |||
assert.NoError(t, PrepareTestDatabase()) | |||
testTeamRepositories := func(teamID int64, repoIds []int64) { | |||
team := AssertExistsAndLoadBean(t, &Team{ID: teamID}).(*Team) | |||
assert.NoError(t, team.GetRepositories(), "%s: GetRepositories", team.Name) | |||
assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) | |||
assert.Equal(t, len(repoIds), len(team.Repos), "%s: repo count", team.Name) | |||
for i, rid := range repoIds { | |||
if rid > 0 { | |||
assert.True(t, team.HasRepository(rid), "%s: HasRepository(%d) %d", rid, i) | |||
} | |||
} | |||
} | |||
// Get an admin user. | |||
user, err := GetUserByID(1) | |||
assert.NoError(t, err, "GetUserByID") | |||
// Create org. | |||
org := &User{ | |||
Name: "All repo", | |||
IsActive: true, | |||
Type: UserTypeOrganization, | |||
Visibility: structs.VisibleTypePublic, | |||
} | |||
assert.NoError(t, CreateOrganization(org, user), "CreateOrganization") | |||
// Check Owner team. | |||
ownerTeam, err := org.GetOwnerTeam() | |||
assert.NoError(t, err, "GetOwnerTeam") | |||
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") | |||
// Create repos. | |||
repoIds := make([]int64, 0) | |||
for i := 0; i < 3; i++ { | |||
r, err := CreateRepository(user, org, CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) | |||
assert.NoError(t, err, "CreateRepository %d", i) | |||
if r != nil { | |||
repoIds = append(repoIds, r.ID) | |||
} | |||
} | |||
// Get fresh copy of Owner team after creating repos. | |||
ownerTeam, err = org.GetOwnerTeam() | |||
assert.NoError(t, err, "GetOwnerTeam") | |||
// Create teams and check repositories. | |||
teams := []*Team{ | |||
ownerTeam, | |||
{ | |||
OrgID: org.ID, | |||
Name: "team one", | |||
Authorize: AccessModeRead, | |||
IncludesAllRepositories: true, | |||
}, | |||
{ | |||
OrgID: org.ID, | |||
Name: "team 2", | |||
Authorize: AccessModeRead, | |||
IncludesAllRepositories: false, | |||
}, | |||
{ | |||
OrgID: org.ID, | |||
Name: "team three", | |||
Authorize: AccessModeWrite, | |||
IncludesAllRepositories: true, | |||
}, | |||
{ | |||
OrgID: org.ID, | |||
Name: "team 4", | |||
Authorize: AccessModeWrite, | |||
IncludesAllRepositories: false, | |||
}, | |||
} | |||
teamRepos := [][]int64{ | |||
repoIds, | |||
repoIds, | |||
{}, | |||
repoIds, | |||
{}, | |||
} | |||
for i, team := range teams { | |||
if i > 0 { // first team is Owner. | |||
assert.NoError(t, NewTeam(team), "%s: NewTeam", team.Name) | |||
} | |||
testTeamRepositories(team.ID, teamRepos[i]) | |||
} | |||
// Update teams and check repositories. | |||
teams[3].IncludesAllRepositories = false | |||
teams[4].IncludesAllRepositories = true | |||
teamRepos[4] = repoIds | |||
for i, team := range teams { | |||
assert.NoError(t, UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) | |||
testTeamRepositories(team.ID, teamRepos[i]) | |||
} | |||
// Create repo and check teams repositories. | |||
org.Teams = nil // Reset teams to allow their reloading. | |||
r, err := CreateRepository(user, org, CreateRepoOptions{Name: "repo-last"}) | |||
assert.NoError(t, err, "CreateRepository last") | |||
if r != nil { | |||
repoIds = append(repoIds, r.ID) | |||
} | |||
teamRepos[0] = repoIds | |||
teamRepos[1] = repoIds | |||
teamRepos[4] = repoIds | |||
for i, team := range teams { | |||
testTeamRepositories(team.ID, teamRepos[i]) | |||
} | |||
// Remove repo and check teams repositories. | |||
assert.NoError(t, DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") | |||
teamRepos[0] = repoIds[1:] | |||
teamRepos[1] = repoIds[1:] | |||
teamRepos[3] = repoIds[1:3] | |||
teamRepos[4] = repoIds[1:] | |||
for i, team := range teams { | |||
testTeamRepositories(team.ID, teamRepos[i]) | |||
} | |||
// Wipe created items. | |||
for i, rid := range repoIds { | |||
if i > 0 { // first repo already deleted. | |||
assert.NoError(t, DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) | |||
} | |||
} | |||
assert.NoError(t, DeleteOrganization(org), "DeleteOrganization") | |||
} |
@@ -1447,14 +1447,17 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err | |||
} | |||
u.NumRepos++ | |||
// Give access to all members in owner team. | |||
// Give access to all members in teams with access to all repositories. | |||
if u.IsOrganization() { | |||
t, err := u.getOwnerTeam(e) | |||
if err != nil { | |||
return fmt.Errorf("getOwnerTeam: %v", err) | |||
if err := u.GetTeams(); err != nil { | |||
return fmt.Errorf("GetTeams: %v", err) | |||
} | |||
if err = t.addRepository(e, repo); err != nil { | |||
return fmt.Errorf("addRepository: %v", err) | |||
for _, t := range u.Teams { | |||
if t.IncludesAllRepositories { | |||
if err := t.addRepository(e, repo); err != nil { | |||
return fmt.Errorf("addRepository: %v", err) | |||
} | |||
} | |||
} | |||
} else if err = repo.recalculateAccesses(e); err != nil { | |||
// Organization automatically called this in addRepository method. | |||
@@ -1641,11 +1644,15 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error | |||
} | |||
if newOwner.IsOrganization() { | |||
t, err := newOwner.getOwnerTeam(sess) | |||
if err != nil { | |||
return fmt.Errorf("getOwnerTeam: %v", err) | |||
} else if err = t.addRepository(sess, repo); err != nil { | |||
return fmt.Errorf("add to owner team: %v", err) | |||
if err := newOwner.GetTeams(); err != nil { | |||
return fmt.Errorf("GetTeams: %v", err) | |||
} | |||
for _, t := range newOwner.Teams { | |||
if t.IncludesAllRepositories { | |||
if err := t.addRepository(sess, repo); err != nil { | |||
return fmt.Errorf("addRepository: %v", err) | |||
} | |||
} | |||
} | |||
} else if err = repo.recalculateAccesses(sess); err != nil { | |||
// Organization called this in addRepository method. |
@@ -62,6 +62,7 @@ type CreateTeamForm struct { | |||
Description string `binding:"MaxSize(255)"` | |||
Permission string | |||
Units []models.UnitType | |||
RepoAccess string | |||
} | |||
// Validate validates the fields |
@@ -7,10 +7,11 @@ package structs | |||
// Team represents a team in an organization | |||
type Team struct { | |||
ID int64 `json:"id"` | |||
Name string `json:"name"` | |||
Description string `json:"description"` | |||
Organization *Organization `json:"organization"` | |||
ID int64 `json:"id"` | |||
Name string `json:"name"` | |||
Description string `json:"description"` | |||
Organization *Organization `json:"organization"` | |||
IncludesAllRepositories bool `json:"includes_all_repositories"` | |||
// enum: none,read,write,admin,owner | |||
Permission string `json:"permission"` | |||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] | |||
@@ -20,8 +21,9 @@ type Team struct { | |||
// CreateTeamOption options for creating a team | |||
type CreateTeamOption struct { | |||
// required: true | |||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` | |||
Description string `json:"description" binding:"MaxSize(255)"` | |||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` | |||
Description string `json:"description" binding:"MaxSize(255)"` | |||
IncludesAllRepositories bool `json:"includes_all_repositories"` | |||
// enum: read,write,admin | |||
Permission string `json:"permission"` | |||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] | |||
@@ -31,8 +33,9 @@ type CreateTeamOption struct { | |||
// EditTeamOption options for editing a team | |||
type EditTeamOption struct { | |||
// required: true | |||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` | |||
Description string `json:"description" binding:"MaxSize(255)"` | |||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` | |||
Description string `json:"description" binding:"MaxSize(255)"` | |||
IncludesAllRepositories bool `json:"includes_all_repositories"` | |||
// enum: read,write,admin | |||
Permission string `json:"permission"` | |||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] |
@@ -1515,6 +1515,7 @@ team_name = Team Name | |||
team_desc = Description | |||
team_name_helper = Team names should be short and memorable. | |||
team_desc_helper = Describe the purpose or role of the team. | |||
team_access_desc = Repository access | |||
team_permission_desc = Permission | |||
team_unit_desc = Allow Access to Repository Sections | |||
@@ -1588,6 +1589,13 @@ teams.add_nonexistent_repo = "The repository you're trying to add does not exist | |||
teams.add_duplicate_users = User is already a team member. | |||
teams.repos.none = No repositories could be accessed by this team. | |||
teams.members.none = No members on this team. | |||
teams.specific_repositories = Specific repositories | |||
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>. | |||
teams.all_repositories = All repositories | |||
teams.all_repositories_helper = Team has access to all repositories. Selecting this will <strong>add all existing</strong> repositories to the team. | |||
teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories. | |||
teams.all_repositories_write_permission_desc = This team grants <strong>Write</strong> access to <strong>all repositories</strong>: members can read from and push to repositories. | |||
teams.all_repositories_admin_permission_desc = This team grants <strong>Admin</strong> access to <strong>all repositories</strong>: members can read from, push to and add collaborators to repositories. | |||
[admin] | |||
dashboard = Dashboard |
@@ -227,11 +227,12 @@ func ToOrganization(org *models.User) *api.Organization { | |||
// ToTeam convert models.Team to api.Team | |||
func ToTeam(team *models.Team) *api.Team { | |||
return &api.Team{ | |||
ID: team.ID, | |||
Name: team.Name, | |||
Description: team.Description, | |||
Permission: team.Authorize.String(), | |||
Units: team.GetUnitNames(), | |||
ID: team.ID, | |||
Name: team.Name, | |||
Description: team.Description, | |||
IncludesAllRepositories: team.IncludesAllRepositories, | |||
Permission: team.Authorize.String(), | |||
Units: team.GetUnitNames(), | |||
} | |||
} | |||
@@ -128,10 +128,11 @@ func CreateTeam(ctx *context.APIContext, form api.CreateTeamOption) { | |||
// "201": | |||
// "$ref": "#/responses/Team" | |||
team := &models.Team{ | |||
OrgID: ctx.Org.Organization.ID, | |||
Name: form.Name, | |||
Description: form.Description, | |||
Authorize: models.ParseAccessMode(form.Permission), | |||
OrgID: ctx.Org.Organization.ID, | |||
Name: form.Name, | |||
Description: form.Description, | |||
IncludesAllRepositories: form.IncludesAllRepositories, | |||
Authorize: models.ParseAccessMode(form.Permission), | |||
} | |||
unitTypes := models.FindUnitTypes(form.Units...) | |||
@@ -182,11 +183,27 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { | |||
// "200": | |||
// "$ref": "#/responses/Team" | |||
team := ctx.Org.Team | |||
team.Name = form.Name | |||
team.Description = form.Description | |||
team.Authorize = models.ParseAccessMode(form.Permission) | |||
unitTypes := models.FindUnitTypes(form.Units...) | |||
isAuthChanged := false | |||
isIncludeAllChanged := false | |||
if !team.IsOwnerTeam() { | |||
// Validate permission level. | |||
auth := models.ParseAccessMode(form.Permission) | |||
team.Name = form.Name | |||
if team.Authorize != auth { | |||
isAuthChanged = true | |||
team.Authorize = auth | |||
} | |||
if team.IncludesAllRepositories != form.IncludesAllRepositories { | |||
isIncludeAllChanged = true | |||
team.IncludesAllRepositories = form.IncludesAllRepositories | |||
} | |||
} | |||
if team.Authorize < models.AccessModeOwner { | |||
var units = make([]*models.TeamUnit, 0, len(form.Units)) | |||
for _, tp := range unitTypes { | |||
@@ -198,7 +215,7 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { | |||
team.Units = units | |||
} | |||
if err := models.UpdateTeam(team, true); err != nil { | |||
if err := models.UpdateTeam(team, isAuthChanged, isIncludeAllChanged); err != nil { | |||
ctx.Error(500, "EditTeam", err) | |||
return | |||
} |
@@ -1,4 +1,5 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Copyright 2019 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
@@ -180,12 +181,14 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) { | |||
ctx.Data["PageIsOrgTeams"] = true | |||
ctx.Data["PageIsOrgTeamsNew"] = true | |||
ctx.Data["Units"] = models.Units | |||
var includesAllRepositories = (form.RepoAccess == "all") | |||
t := &models.Team{ | |||
OrgID: ctx.Org.Organization.ID, | |||
Name: form.TeamName, | |||
Description: form.Description, | |||
Authorize: models.ParseAccessMode(form.Permission), | |||
OrgID: ctx.Org.Organization.ID, | |||
Name: form.TeamName, | |||
Description: form.Description, | |||
Authorize: models.ParseAccessMode(form.Permission), | |||
IncludesAllRepositories: includesAllRepositories, | |||
} | |||
if t.Authorize < models.AccessModeOwner { | |||
@@ -268,6 +271,8 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { | |||
ctx.Data["Units"] = models.Units | |||
isAuthChanged := false | |||
isIncludeAllChanged := false | |||
var includesAllRepositories = (form.RepoAccess == "all") | |||
if !t.IsOwnerTeam() { | |||
// Validate permission level. | |||
auth := models.ParseAccessMode(form.Permission) | |||
@@ -277,6 +282,11 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { | |||
isAuthChanged = true | |||
t.Authorize = auth | |||
} | |||
if t.IncludesAllRepositories != includesAllRepositories { | |||
isIncludeAllChanged = true | |||
t.IncludesAllRepositories = includesAllRepositories | |||
} | |||
} | |||
t.Description = form.Description | |||
if t.Authorize < models.AccessModeOwner { | |||
@@ -305,7 +315,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { | |||
return | |||
} | |||
if err := models.UpdateTeam(t, isAuthChanged); err != nil { | |||
if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil { | |||
ctx.Data["Err_TeamName"] = true | |||
switch { | |||
case models.IsErrTeamAlreadyExist(err): |
@@ -24,6 +24,24 @@ | |||
<span class="help">{{.i18n.Tr "org.team_desc_helper"}}</span> | |||
</div> | |||
{{if not (eq .Team.LowerName "owners")}} | |||
<div class="grouped field"> | |||
<label>{{.i18n.Tr "org.team_access_desc"}}</label> | |||
<br> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}> | |||
<label>{{.i18n.Tr "org.teams.specific_repositories"}}</label> | |||
<span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper"}}</span> | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="ui radio checkbox"> | |||
<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}> | |||
<label>{{.i18n.Tr "org.teams.all_repositories"}}</label> | |||
<span class="help">{{.i18n.Tr "org.teams.all_repositories_helper"}}</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="grouped field"> | |||
<label>{{.i18n.Tr "org.team_permission_desc"}}</label> | |||
<br> |
@@ -7,7 +7,7 @@ | |||
{{template "org/team/sidebar" .}} | |||
<div class="ui ten wide column"> | |||
{{template "org/team/navbar" .}} | |||
{{$canAddRemove := and $.IsOrganizationOwner (not (eq $.Team.LowerName "owners"))}} | |||
{{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}} | |||
{{if $canAddRemove}} | |||
<div class="ui attached segment"> | |||
<form class="ui form" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/add" method="post"> |
@@ -22,11 +22,23 @@ | |||
{{if eq .Team.LowerName "owners"}} | |||
{{.i18n.Tr "org.teams.owners_permission_desc" | Str2html}} | |||
{{else if (eq .Team.Authorize 1)}} | |||
{{.i18n.Tr "org.teams.read_permission_desc" | Str2html}} | |||
{{if .Team.IncludesAllRepositories}} | |||
{{.i18n.Tr "org.teams.all_repositories_read_permission_desc" | Str2html}} | |||
{{else}} | |||
{{.i18n.Tr "org.teams.read_permission_desc" | Str2html}} | |||
{{end}} | |||
{{else if (eq .Team.Authorize 2)}} | |||
{{.i18n.Tr "org.teams.write_permission_desc" | Str2html}} | |||
{{if .Team.IncludesAllRepositories}} | |||
{{.i18n.Tr "org.teams.all_repositories_write_permission_desc" | Str2html}} | |||
{{else}} | |||
{{.i18n.Tr "org.teams.write_permission_desc" | Str2html}} | |||
{{end}} | |||
{{else if (eq .Team.Authorize 3)}} | |||
{{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} | |||
{{if .Team.IncludesAllRepositories}} | |||
{{.i18n.Tr "org.teams.all_repositories_admin_permission_desc" | Str2html}} | |||
{{else}} | |||
{{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> |
@@ -8242,6 +8242,10 @@ | |||
"type": "string", | |||
"x-go-name": "Description" | |||
}, | |||
"includes_all_repositories": { | |||
"type": "boolean", | |||
"x-go-name": "IncludesAllRepositories" | |||
}, | |||
"name": { | |||
"type": "string", | |||
"x-go-name": "Name" | |||
@@ -8801,6 +8805,10 @@ | |||
"type": "string", | |||
"x-go-name": "Description" | |||
}, | |||
"includes_all_repositories": { | |||
"type": "boolean", | |||
"x-go-name": "IncludesAllRepositories" | |||
}, | |||
"name": { | |||
"type": "string", | |||
"x-go-name": "Name" | |||
@@ -10457,6 +10465,10 @@ | |||
"format": "int64", | |||
"x-go-name": "ID" | |||
}, | |||
"includes_all_repositories": { | |||
"type": "boolean", | |||
"x-go-name": "IncludesAllRepositories" | |||
}, | |||
"name": { | |||
"type": "string", | |||
"x-go-name": "Name" |