]> source.dussan.org Git - gitea.git/commitdiff
Add team option to grant rights for all organization repositories (#8688)
authorDavid Svantesson <davidsvantesson@gmail.com>
Wed, 6 Nov 2019 09:37:14 +0000 (10:37 +0100)
committerLauris BH <lauris@nix.lv>
Wed, 6 Nov 2019 09:37:14 +0000 (11:37 +0200)
* 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 753b7d205be260b8be465b5291a02975a81f3093.

* Clarify help text what options do.

17 files changed:
integrations/api_team_test.go
models/migrations/migrations.go
models/migrations/v105.go [new file with mode: 0644]
models/org.go
models/org_team.go
models/org_team_test.go
models/repo.go
modules/auth/org.go
modules/structs/org_team.go
options/locale/locale_en-US.ini
routers/api/v1/convert/convert.go
routers/api/v1/org/team.go
routers/org/teams.go
templates/org/team/new.tmpl
templates/org/team/repositories.tmpl
templates/org/team/sidebar.tmpl
templates/swagger/v1_json.tmpl

index 38e202f239f8f6de84b5ef70e86522fb2ca71b78..e25ffdf7b12540269d11afa1cb841520b18fd2ae 100644 (file)
@@ -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 {
index e214f16a2a1e1e08676c33b588d314496f3ca734..5ed70dc4f5f4e27a9f614dee7522e6777f62b829 100644 (file)
@@ -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
diff --git a/models/migrations/v105.go b/models/migrations/v105.go
new file mode 100644 (file)
index 0000000..6c9a581
--- /dev/null
@@ -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
+}
index 2cc302dac60eafeb521f498fb80d41fb55375278..78b035b10147b30064f4fd4c8c6a36a8de982e74 100644 (file)
@@ -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)
index a7a179f1044f9a56921113da163583ba168c3bb9..d740e1c24075fbd0111fc7ee9d79b365f9ffffb9 100644 (file)
@@ -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()
 }
 
index 06ab4637d8c2bc6377314431599c90a23ef96282..b7e2ef113d3d9ebedd7ccdb64303b5bfd1d78e11 100644 (file)
@@ -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")
+}
index 7945cb309d3417c9de655ebde833bf7c1271a854..89e579d1ec8fd14019ceb8329c1102da91b42bf2 100644 (file)
@@ -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.
index 94e659cb5bd6cfb94d0d75cef41c6cd80a43878b..509358882a3852c4b828a0f7f4afbd93754f75db 100644 (file)
@@ -62,6 +62,7 @@ type CreateTeamForm struct {
        Description string `binding:"MaxSize(255)"`
        Permission  string
        Units       []models.UnitType
+       RepoAccess  string
 }
 
 // Validate validates the fields
index bea4a10ad494db292a05e85d164f078d6b323788..5053468b4a53d2a1a71170a3680437af2c3d2c93 100644 (file)
@@ -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"]
index db28fcede037c536daf66d9c8524844a74bcaf1c..932d0bceac93a213a251c21a420b81386c67a884 100644 (file)
@@ -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
index 6da53d62750c056fe14b2a98708f0f512d88dd3d..f52ed63476dcb753e7858c55760ef0d61e08230a 100644 (file)
@@ -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(),
        }
 }
 
index d01f051626f7b01eeb3717f236f0cb6df745131a..a22b60a2c68039bf4b9debb05bd50aac11b6d7aa 100644 (file)
@@ -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
        }
index 7ead6ea5ff8ca7c1a078f584afa62675011777ae..24612459a4fe7f80ff91b6c5fbaad5c713c4a8dc 100644 (file)
@@ -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):
index fb79c9b7fbac562474e6045e003ea8a07b4b3e31..e50a1777d2002898b51bb0f4c6d6c70fc97a170f 100644 (file)
                                                <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>
index eeb86564fd4a24611092a5968e1d1f082cfd345b..1b2a411c2bb0d294fa43b8e43b87ea66e536afc4 100644 (file)
@@ -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">
index 846613e32e77f2588043650003455da0afde8ef7..dd189df5f313b585131ee58a1b7ee2ac3b18399e 100644 (file)
                        {{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>
index 17b8eab6c5e2834f764b39a2596103338d5965a9..dc162bc37d3492407f6d3555bf419381eaafbdaf 100644 (file)
           "type": "string",
           "x-go-name": "Description"
         },
+        "includes_all_repositories": {
+          "type": "boolean",
+          "x-go-name": "IncludesAllRepositories"
+        },
         "name": {
           "type": "string",
           "x-go-name": "Name"
           "type": "string",
           "x-go-name": "Description"
         },
+        "includes_all_repositories": {
+          "type": "boolean",
+          "x-go-name": "IncludesAllRepositories"
+        },
         "name": {
           "type": "string",
           "x-go-name": "Name"
           "format": "int64",
           "x-go-name": "ID"
         },
+        "includes_all_repositories": {
+          "type": "boolean",
+          "x-go-name": "IncludesAllRepositories"
+        },
         "name": {
           "type": "string",
           "x-go-name": "Name"