summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/error.go17
-rw-r--r--models/fixtures/repository.yml11
-rw-r--r--models/fixtures/team.yml9
-rw-r--r--models/fixtures/user.yml18
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v97.go15
-rw-r--r--models/org.go8
-rw-r--r--models/org_team.go4
-rw-r--r--models/org_test.go6
-rw-r--r--models/repo_collaboration.go28
-rw-r--r--models/repo_collaboration_test.go11
-rw-r--r--models/user.go13
-rw-r--r--models/user_test.go5
-rw-r--r--models/userlist.go2
-rw-r--r--modules/auth/org.go15
-rw-r--r--modules/structs/org.go23
-rw-r--r--options/locale/locale_en-US.ini9
-rw-r--r--public/css/index.css3
-rw-r--r--public/js/index.js25
-rw-r--r--public/less/_repository.less23
-rw-r--r--routers/api/v1/convert/convert.go17
-rw-r--r--routers/api/v1/org/org.go17
-rw-r--r--routers/org/setting.go1
-rw-r--r--routers/repo/setting.go83
-rw-r--r--routers/repo/settings_test.go193
-rw-r--r--routers/routes/routes.go4
-rw-r--r--templates/org/create.tmpl11
-rw-r--r--templates/org/settings/options.tmpl10
-rw-r--r--templates/repo/settings/collaboration.tmpl59
-rw-r--r--templates/swagger/v1_json.tmpl12
30 files changed, 575 insertions, 79 deletions
diff --git a/models/error.go b/models/error.go
index cecd03f993..c025437c50 100644
--- a/models/error.go
+++ b/models/error.go
@@ -1370,6 +1370,23 @@ func (err ErrTeamAlreadyExist) Error() string {
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
}
+// ErrTeamNotExist represents a "TeamNotExist" error
+type ErrTeamNotExist struct {
+ OrgID int64
+ TeamID int64
+ Name string
+}
+
+// IsErrTeamNotExist checks if an error is a ErrTeamNotExist.
+func IsErrTeamNotExist(err error) bool {
+ _, ok := err.(ErrTeamNotExist)
+ return ok
+}
+
+func (err ErrTeamNotExist) Error() string {
+ return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name)
+}
+
//
// Two-factor authentication
//
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index e1370aa098..2e38c5e1dd 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -508,4 +508,15 @@
num_stars: 0
num_forks: 0
num_issues: 0
+ is_mirror: false
+
+-
+ id: 43
+ owner_id: 26
+ lower_name: repo26
+ name: repo26
+ is_private: true
+ num_stars: 0
+ num_forks: 0
+ num_issues: 0
is_mirror: false \ No newline at end of file
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index b7265ec49e..4da87b731f 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -87,3 +87,12 @@
authorize: 1 # owner
num_repos: 0
num_members: 1
+
+-
+ id: 11
+ org_id: 26
+ lower_name: team11
+ name: team11
+ authorize: 1 # read
+ num_repos: 0
+ num_members: 0
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 5177173e7e..a204241f9c 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -410,3 +410,21 @@
num_repos: 0
num_members: 1
num_teams: 1
+
+-
+ id: 26
+ lower_name: org26
+ name: org26
+ full_name: "Org26"
+ email: org26@example.com
+ email_notifications_preference: onmention
+ passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
+ type: 1 # organization
+ salt: ZogKvWdyEx
+ is_admin: false
+ avatar: avatar26
+ avatar_email: org26@example.com
+ num_repos: 1
+ num_members: 0
+ num_teams: 1
+ repo_admin_change_team_access: true \ No newline at end of file
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 8717da789e..7680e7747c 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -248,6 +248,8 @@ var migrations = []Migration{
NewMigration("add table columns for cross referencing issues", addCrossReferenceColumns),
// v96 -> v97
NewMigration("delete orphaned attachments", deleteOrphanedAttachments),
+ // v97 -> v98
+ NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser),
}
// Migrate database to current version
diff --git a/models/migrations/v97.go b/models/migrations/v97.go
new file mode 100644
index 0000000000..fa542f2ccd
--- /dev/null
+++ b/models/migrations/v97.go
@@ -0,0 +1,15 @@
+// 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 "github.com/go-xorm/xorm"
+
+func addRepoAdminChangeTeamAccessColumnForUser(x *xorm.Engine) error {
+ type User struct {
+ RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
+ }
+
+ return x.Sync2(new(User))
+}
diff --git a/models/org.go b/models/org.go
index e00bef5798..ca3bce81a1 100644
--- a/models/org.go
+++ b/models/org.go
@@ -6,7 +6,6 @@
package models
import (
- "errors"
"fmt"
"os"
"strings"
@@ -20,11 +19,6 @@ import (
"xorm.io/builder"
)
-var (
- // ErrTeamNotExist team does not exist
- ErrTeamNotExist = errors.New("Team does not exist")
-)
-
// IsOwnedBy returns true if given user is in the owner team.
func (org *User) IsOwnedBy(uid int64) (bool, error) {
return IsOrganizationOwner(org.ID, uid)
@@ -304,7 +298,7 @@ type OrgUser struct {
func isOrganizationOwner(e Engine, orgID, uid int64) (bool, error) {
ownerTeam, err := getOwnerTeam(e, orgID)
if err != nil {
- if err == ErrTeamNotExist {
+ if IsErrTeamNotExist(err) {
log.Error("Organization does not have owner team: %d", orgID)
return false, nil
}
diff --git a/models/org_team.go b/models/org_team.go
index 799716679c..90a089417d 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -352,7 +352,7 @@ func getTeam(e Engine, orgID int64, name string) (*Team, error) {
if err != nil {
return nil, err
} else if !has {
- return nil, ErrTeamNotExist
+ return nil, ErrTeamNotExist{orgID, 0, name}
}
return t, nil
}
@@ -373,7 +373,7 @@ func getTeamByID(e Engine, teamID int64) (*Team, error) {
if err != nil {
return nil, err
} else if !has {
- return nil, ErrTeamNotExist
+ return nil, ErrTeamNotExist{0, teamID, ""}
}
return t, nil
}
diff --git a/models/org_test.go b/models/org_test.go
index 6c1c4fee0e..2f2c5a2d5e 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -64,11 +64,11 @@ func TestUser_GetTeam(t *testing.T) {
assert.Equal(t, "team1", team.LowerName)
_, err = org.GetTeam("does not exist")
- assert.Equal(t, ErrTeamNotExist, err)
+ assert.True(t, IsErrTeamNotExist(err))
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
_, err = nonOrg.GetTeam("team")
- assert.Equal(t, ErrTeamNotExist, err)
+ assert.True(t, IsErrTeamNotExist(err))
}
func TestUser_GetOwnerTeam(t *testing.T) {
@@ -80,7 +80,7 @@ func TestUser_GetOwnerTeam(t *testing.T) {
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
_, err = nonOrg.GetOwnerTeam()
- assert.Equal(t, ErrTeamNotExist, err)
+ assert.True(t, IsErrTeamNotExist(err))
}
func TestUser_GetTeams(t *testing.T) {
diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go
index 0797f50430..40ddf6a28c 100644
--- a/models/repo_collaboration.go
+++ b/models/repo_collaboration.go
@@ -16,20 +16,6 @@ type Collaboration struct {
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
}
-// ModeI18nKey returns the collaboration mode I18n Key
-func (c *Collaboration) ModeI18nKey() string {
- switch c.Mode {
- case AccessModeRead:
- return "repo.settings.collaboration.read"
- case AccessModeWrite:
- return "repo.settings.collaboration.write"
- case AccessModeAdmin:
- return "repo.settings.collaboration.admin"
- default:
- return "repo.settings.collaboration.undefined"
- }
-}
-
// AddCollaborator adds new collaboration to a repository with default access mode.
func (repo *Repository) AddCollaborator(u *User) error {
collaboration := &Collaboration{
@@ -183,3 +169,17 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
return sess.Commit()
}
+
+func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) {
+ return teams, e.
+ Join("INNER", "team_repo", "team_repo.team_id = team.id").
+ Where("team.org_id = ?", repo.OwnerID).
+ And("team_repo.repo_id=?", repo.ID).
+ OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END").
+ Find(&teams)
+}
+
+// GetRepoTeams gets the list of teams that has access to the repository
+func (repo *Repository) GetRepoTeams() ([]*Team, error) {
+ return repo.getRepoTeams(x)
+}
diff --git a/models/repo_collaboration_test.go b/models/repo_collaboration_test.go
index f11f3c54c3..0842212460 100644
--- a/models/repo_collaboration_test.go
+++ b/models/repo_collaboration_test.go
@@ -10,17 +10,6 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestCollaboration_ModeI18nKey(t *testing.T) {
- assert.Equal(t, "repo.settings.collaboration.read",
- (&Collaboration{Mode: AccessModeRead}).ModeI18nKey())
- assert.Equal(t, "repo.settings.collaboration.write",
- (&Collaboration{Mode: AccessModeWrite}).ModeI18nKey())
- assert.Equal(t, "repo.settings.collaboration.admin",
- (&Collaboration{Mode: AccessModeAdmin}).ModeI18nKey())
- assert.Equal(t, "repo.settings.collaboration.undefined",
- (&Collaboration{Mode: AccessModeNone}).ModeI18nKey())
-}
-
func TestRepository_AddCollaborator(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
diff --git a/models/user.go b/models/user.go
index af4ccacf6f..5e87473e86 100644
--- a/models/user.go
+++ b/models/user.go
@@ -147,12 +147,13 @@ type User struct {
NumRepos int
// For organization
- NumTeams int
- NumMembers int
- Teams []*Team `xorm:"-"`
- Members UserList `xorm:"-"`
- MembersIsPublic map[int64]bool `xorm:"-"`
- Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
+ NumTeams int
+ NumMembers int
+ Teams []*Team `xorm:"-"`
+ Members UserList `xorm:"-"`
+ MembersIsPublic map[int64]bool `xorm:"-"`
+ Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
+ RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
// Preferences
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
diff --git a/models/user_test.go b/models/user_test.go
index d01b482ae8..bcb955817c 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -140,7 +140,10 @@ func TestSearchUsers(t *testing.T) {
testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2},
[]int64{19, 25})
- testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2},
+ testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 4, PageSize: 2},
+ []int64{26})
+
+ testOrgSuccess(&SearchUserOptions{Page: 5, PageSize: 2},
[]int64{})
// test users
diff --git a/models/userlist.go b/models/userlist.go
index 43838a6804..a2a4248482 100644
--- a/models/userlist.go
+++ b/models/userlist.go
@@ -43,7 +43,7 @@ func (users UserList) loadOrganizationOwners(e Engine, orgID int64) (map[int64]*
}
ownerTeam, err := getOwnerTeam(e, orgID)
if err != nil {
- if err == ErrTeamNotExist {
+ if IsErrTeamNotExist(err) {
log.Error("Organization does not have owner team: %d", orgID)
return nil, nil
}
diff --git a/modules/auth/org.go b/modules/auth/org.go
index 367468e587..2abffdf74e 100644
--- a/modules/auth/org.go
+++ b/modules/auth/org.go
@@ -33,13 +33,14 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind
// UpdateOrgSettingForm form for updating organization settings
type UpdateOrgSettingForm struct {
- Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
- FullName string `binding:"MaxSize(100)"`
- Description string `binding:"MaxSize(255)"`
- Website string `binding:"ValidUrl;MaxSize(255)"`
- Location string `binding:"MaxSize(50)"`
- Visibility structs.VisibleType
- MaxRepoCreation int
+ Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
+ FullName string `binding:"MaxSize(100)"`
+ Description string `binding:"MaxSize(255)"`
+ Website string `binding:"ValidUrl;MaxSize(255)"`
+ Location string `binding:"MaxSize(50)"`
+ Visibility structs.VisibleType
+ MaxRepoCreation int
+ RepoAdminChangeTeamAccess bool
}
// Validate validates the fields
diff --git a/modules/structs/org.go b/modules/structs/org.go
index 08ab139975..4b79a4e70a 100644
--- a/modules/structs/org.go
+++ b/modules/structs/org.go
@@ -6,14 +6,15 @@ package structs
// Organization represents an organization
type Organization struct {
- ID int64 `json:"id"`
- UserName string `json:"username"`
- FullName string `json:"full_name"`
- AvatarURL string `json:"avatar_url"`
- Description string `json:"description"`
- Website string `json:"website"`
- Location string `json:"location"`
- Visibility string `json:"visibility"`
+ ID int64 `json:"id"`
+ UserName string `json:"username"`
+ FullName string `json:"full_name"`
+ AvatarURL string `json:"avatar_url"`
+ Description string `json:"description"`
+ Website string `json:"website"`
+ Location string `json:"location"`
+ Visibility string `json:"visibility"`
+ RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
}
// CreateOrgOption options for creating an organization
@@ -26,7 +27,8 @@ type CreateOrgOption struct {
Location string `json:"location"`
// possible values are `public` (default), `limited` or `private`
// enum: public,limited,private
- Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
+ Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
+ RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
}
// EditOrgOption options for editing an organization
@@ -37,5 +39,6 @@ type EditOrgOption struct {
Location string `json:"location"`
// possible values are `public`, `limited` or `private`
// enum: public,limited,private
- Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
+ Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
+ RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 52ccea68b2..7bb4539684 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -319,6 +319,7 @@ enterred_invalid_repo_name = The repository name you entered is incorrect.
enterred_invalid_owner_name = The new owner name is not valid.
enterred_invalid_password = The password you entered is incorrect.
user_not_exist = The user does not exist.
+team_not_exist = The team does not exist.
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner in any given team.
cannot_add_org_to_team = An organization cannot be added as a team member.
@@ -1136,6 +1137,7 @@ settings.collaboration = Collaborators
settings.collaboration.admin = Administrator
settings.collaboration.write = Write
settings.collaboration.read = Read
+settings.collaboration.owner = Owner
settings.collaboration.undefined = Undefined
settings.hooks = Webhooks
settings.githooks = Git Hooks
@@ -1217,6 +1219,11 @@ settings.collaborator_deletion_desc = Removing a collaborator will revoke their
settings.remove_collaborator_success = The collaborator has been removed.
settings.search_user_placeholder = Search user…
settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator.
+settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner
+settings.team_not_in_organization = The team is not in the same organization as the repository
+settings.add_team_duplicate = Team already has the repository
+settings.add_team_success = The team now have access to the repository.
+settings.remove_team_success = The team's access to the repository has been removed.
settings.add_webhook = Add Webhook
settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character.
settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>.
@@ -1475,6 +1482,8 @@ settings.options = Organization
settings.full_name = Full Name
settings.website = Website
settings.location = Location
+settings.permission = Permissions
+settings.repoadminchangeteam = Repository admin can add and remove access for teams
settings.visibility = Visibility
settings.visibility.public = Public
settings.visibility.limited = Limited (Visible to logged in users only)
diff --git a/public/css/index.css b/public/css/index.css
index 8f24e7b3e4..1da2399c46 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -747,6 +747,8 @@ footer .ui.left,footer .ui.right{line-height:40px}
.repository.settings.collaboration .collaborator.list>.item:not(:last-child){border-bottom:1px solid #ddd}
.repository.settings.collaboration #repo-collab-form #search-user-box .results{left:7px}
.repository.settings.collaboration #repo-collab-form .ui.button{margin-left:5px;margin-top:-3px}
+.repository.settings.collaboration #repo-collab-team-form #search-team-box .results{left:7px}
+.repository.settings.collaboration #repo-collab-team-form .ui.button{margin-left:5px;margin-top:-3px}
.repository.settings.branches .protected-branches .selection.dropdown{width:300px}
.repository.settings.branches .protected-branches .item{border:1px solid #eaeaea;padding:10px 15px}
.repository.settings.branches .protected-branches .item:not(:last-child){border-bottom:0}
@@ -783,6 +785,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
.user-cards .list .item .meta{margin-top:5px}
#search-repo-box .results .result .image,#search-user-box .results .result .image{float:left;margin-right:8px;width:2em;height:2em}
#search-repo-box .results .result .content,#search-user-box .results .result .content{margin:6px 0}
+#search-team-box .results .result .content{margin:6px 0}
#issue-filters.hide{display:none}
#issue-actions{margin-top:-1rem!important}
#issue-actions.hide{display:none}
diff --git a/public/js/index.js b/public/js/index.js
index d99457514b..ad5e3912de 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1761,6 +1761,30 @@ function searchUsers() {
});
}
+function searchTeams() {
+ const $searchTeamBox = $('#search-team-box');
+ $searchTeamBox.search({
+ minCharacters: 2,
+ apiSettings: {
+ url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams',
+ headers: {"X-Csrf-Token": csrf},
+ onResponse: function(response) {
+ const items = [];
+ $.each(response, function (_i, item) {
+ const title = item.name + ' (' + item.permission + ' access)';
+ items.push({
+ title: title,
+ })
+ });
+
+ return { results: items }
+ }
+ },
+ searchFields: ['name', 'description'],
+ showNoResults: false
+ });
+}
+
function searchRepositories() {
const $searchRepoBox = $('#search-repo-box');
$searchRepoBox.search({
@@ -2171,6 +2195,7 @@ $(document).ready(function () {
buttonsClickOnEnter();
searchUsers();
+ searchTeams();
searchRepositories();
initCommentForm();
diff --git a/public/less/_repository.less b/public/less/_repository.less
index 4823d1000a..fde11f7a4d 100644
--- a/public/less/_repository.less
+++ b/public/less/_repository.less
@@ -1736,6 +1736,19 @@
margin-top: -3px;
}
}
+
+ #repo-collab-team-form {
+ #search-team-box {
+ .results {
+ left: 7px;
+ }
+ }
+
+ .ui.button {
+ margin-left: 5px;
+ margin-top: -3px;
+ }
+ }
}
&.branches {
@@ -1936,6 +1949,16 @@
}
}
+#search-team-box {
+ .results {
+ .result {
+ .content {
+ margin: 6px 0;
+ }
+ }
+ }
+}
+
#issue-filters.hide {
display: none;
}
diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go
index 40e4ca7ae3..e0e7f609c7 100644
--- a/routers/api/v1/convert/convert.go
+++ b/routers/api/v1/convert/convert.go
@@ -206,14 +206,15 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey {
// ToOrganization convert models.User to api.Organization
func ToOrganization(org *models.User) *api.Organization {
return &api.Organization{
- ID: org.ID,
- AvatarURL: org.AvatarLink(),
- UserName: org.Name,
- FullName: org.FullName,
- Description: org.Description,
- Website: org.Website,
- Location: org.Location,
- Visibility: org.Visibility.String(),
+ ID: org.ID,
+ AvatarURL: org.AvatarLink(),
+ UserName: org.Name,
+ FullName: org.FullName,
+ Description: org.Description,
+ Website: org.Website,
+ Location: org.Location,
+ Visibility: org.Visibility.String(),
+ RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
}
}
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index 3adc204d3b..8a1a478ba1 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -95,14 +95,15 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) {
}
org := &models.User{
- Name: form.UserName,
- FullName: form.FullName,
- Description: form.Description,
- Website: form.Website,
- Location: form.Location,
- IsActive: true,
- Type: models.UserTypeOrganization,
- Visibility: visibility,
+ Name: form.UserName,
+ FullName: form.FullName,
+ Description: form.Description,
+ Website: form.Website,
+ Location: form.Location,
+ IsActive: true,
+ Type: models.UserTypeOrganization,
+ Visibility: visibility,
+ RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
}
if err := models.CreateOrganization(org, ctx.User); err != nil {
if models.IsErrUserAlreadyExist(err) ||
diff --git a/routers/org/setting.go b/routers/org/setting.go
index 1d534ec558..7de784c5b8 100644
--- a/routers/org/setting.go
+++ b/routers/org/setting.go
@@ -83,6 +83,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) {
org.Website = form.Website
org.Location = form.Location
org.Visibility = form.Visibility
+ org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
if err := models.UpdateUser(org); err != nil {
ctx.ServerError("UpdateUser", err)
return
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 3dc5a1e099..91db519d62 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -490,6 +490,18 @@ func Collaboration(ctx *context.Context) {
}
ctx.Data["Collaborators"] = users
+ teams, err := ctx.Repo.Repository.GetRepoTeams()
+ if err != nil {
+ ctx.ServerError("GetRepoTeams", err)
+ return
+ }
+ ctx.Data["Teams"] = teams
+ ctx.Data["Repo"] = ctx.Repo.Repository
+ ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
+ ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
+ ctx.Data["Org"] = ctx.Repo.Repository.Owner
+ ctx.Data["Units"] = models.Units
+
ctx.HTML(200, tplCollaboration)
}
@@ -566,6 +578,77 @@ func DeleteCollaboration(ctx *context.Context) {
})
}
+// AddTeamPost response for adding a team to a repository
+func AddTeamPost(ctx *context.Context) {
+ if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
+ if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ team, err := ctx.Repo.Owner.GetTeam(name)
+ if err != nil {
+ if models.IsErrTeamNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ } else {
+ ctx.ServerError("GetTeam", err)
+ }
+ return
+ }
+
+ if team.OrgID != ctx.Repo.Repository.OwnerID {
+ ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ if err = team.AddRepository(ctx.Repo.Repository); err != nil {
+ ctx.ServerError("team.AddRepository", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+}
+
+// DeleteTeam response for deleting a team from a repository
+func DeleteTeam(ctx *context.Context) {
+ if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ team, err := models.GetTeamByID(ctx.QueryInt64("id"))
+ if err != nil {
+ ctx.ServerError("GetTeamByID", err)
+ return
+ }
+
+ if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("team.RemoveRepositorys", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
+ })
+}
+
// parseOwnerAndRepo get repos by owner
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
diff --git a/routers/repo/settings_test.go b/routers/repo/settings_test.go
index cf7ed840a8..a05a96cea2 100644
--- a/routers/repo/settings_test.go
+++ b/routers/repo/settings_test.go
@@ -185,3 +185,196 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
+
+func TestAddTeamPost(t *testing.T) {
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "org26/repo43")
+
+ ctx.Req.Form.Set("team", "team11")
+
+ org := &models.User{
+ LowerName: "org26",
+ Type: models.UserTypeOrganization,
+ }
+
+ team := &models.Team{
+ ID: 11,
+ OrgID: 26,
+ }
+
+ re := &models.Repository{
+ ID: 43,
+ Owner: org,
+ OwnerID: 26,
+ }
+
+ repo := &context.Repository{
+ Owner: &models.User{
+ ID: 26,
+ LowerName: "org26",
+ RepoAdminChangeTeamAccess: true,
+ },
+ Repository: re,
+ }
+
+ ctx.Repo = repo
+
+ AddTeamPost(ctx)
+
+ assert.True(t, team.HasRepository(re.ID))
+ assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+ assert.Empty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestAddTeamPost_NotAllowed(t *testing.T) {
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "org26/repo43")
+
+ ctx.Req.Form.Set("team", "team11")
+
+ org := &models.User{
+ LowerName: "org26",
+ Type: models.UserTypeOrganization,
+ }
+
+ team := &models.Team{
+ ID: 11,
+ OrgID: 26,
+ }
+
+ re := &models.Repository{
+ ID: 43,
+ Owner: org,
+ OwnerID: 26,
+ }
+
+ repo := &context.Repository{
+ Owner: &models.User{
+ ID: 26,
+ LowerName: "org26",
+ RepoAdminChangeTeamAccess: false,
+ },
+ Repository: re,
+ }
+
+ ctx.Repo = repo
+
+ AddTeamPost(ctx)
+
+ assert.False(t, team.HasRepository(re.ID))
+ assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+ assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+
+}
+
+func TestAddTeamPost_AddTeamTwice(t *testing.T) {
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "org26/repo43")
+
+ ctx.Req.Form.Set("team", "team11")
+
+ org := &models.User{
+ LowerName: "org26",
+ Type: models.UserTypeOrganization,
+ }
+
+ team := &models.Team{
+ ID: 11,
+ OrgID: 26,
+ }
+
+ re := &models.Repository{
+ ID: 43,
+ Owner: org,
+ OwnerID: 26,
+ }
+
+ repo := &context.Repository{
+ Owner: &models.User{
+ ID: 26,
+ LowerName: "org26",
+ RepoAdminChangeTeamAccess: true,
+ },
+ Repository: re,
+ }
+
+ ctx.Repo = repo
+
+ AddTeamPost(ctx)
+
+ AddTeamPost(ctx)
+ assert.True(t, team.HasRepository(re.ID))
+ assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+ assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestAddTeamPost_NonExistentTeam(t *testing.T) {
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "org26/repo43")
+
+ ctx.Req.Form.Set("team", "team-non-existent")
+
+ org := &models.User{
+ LowerName: "org26",
+ Type: models.UserTypeOrganization,
+ }
+
+ re := &models.Repository{
+ ID: 43,
+ Owner: org,
+ OwnerID: 26,
+ }
+
+ repo := &context.Repository{
+ Owner: &models.User{
+ ID: 26,
+ LowerName: "org26",
+ RepoAdminChangeTeamAccess: true,
+ },
+ Repository: re,
+ }
+
+ ctx.Repo = repo
+
+ AddTeamPost(ctx)
+ assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+ assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestDeleteTeam(t *testing.T) {
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "org3/team1/repo3")
+
+ ctx.Req.Form.Set("id", "2")
+
+ org := &models.User{
+ LowerName: "org3",
+ Type: models.UserTypeOrganization,
+ }
+
+ team := &models.Team{
+ ID: 2,
+ OrgID: 3,
+ }
+
+ re := &models.Repository{
+ ID: 3,
+ Owner: org,
+ OwnerID: 3,
+ }
+
+ repo := &context.Repository{
+ Owner: &models.User{
+ ID: 3,
+ LowerName: "org3",
+ RepoAdminChangeTeamAccess: true,
+ },
+ Repository: re,
+ }
+
+ ctx.Repo = repo
+
+ DeleteTeam(ctx)
+
+ assert.False(t, team.HasRepository(re.ID))
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 2afd0dcce9..93ce220b00 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -629,6 +629,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
m.Post("/delete", repo.DeleteCollaboration)
+ m.Group("/team", func() {
+ m.Post("", repo.AddTeamPost)
+ m.Post("/delete", repo.DeleteTeam)
+ })
})
m.Group("/branches", func() {
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl
index 5b6080c227..8ae0fc22af 100644
--- a/templates/org/create.tmpl
+++ b/templates/org/create.tmpl
@@ -32,6 +32,17 @@
</div>
</div>
</div>
+
+ <div class="field" id="permission_box">
+ <label>{{.i18n.Tr "org.settings.permission"}}</label>
+ <div class="field">
+ <div class="ui checkbox">
+ <input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/>
+ <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label>
+ </div>
+ </div>
+ </div>
+
<div class="inline field">
<label></label>
<button class="ui green button">
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index e8cfd97d4e..28660fe365 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -56,6 +56,16 @@
</div>
</div>
+ <div class="field" id="permission_box">
+ <label>{{.i18n.Tr "org.settings.permission"}}</label>
+ <div class="field">
+ <div class="ui checkbox">
+ <input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/>
+ <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label>
+ </div>
+ </div>
+ </div>
+
{{if .SignedUser.IsAdmin}}
<div class="ui divider"></div>
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 4c3c1a5d74..b65fd4e167 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -20,7 +20,7 @@
<div class="ui eight wide column">
<span class="octicon octicon-shield"></span>
<div class="ui inline dropdown">
- <div class="text">{{$.i18n.Tr .Collaboration.ModeI18nKey}}</div>
+ <div class="text">{{if eq .Collaboration.Mode 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Collaboration.Mode 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Collaboration.Mode 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div>
<i class="dropdown icon"></i>
<div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}">
<div class="item" data-text="{{$.i18n.Tr "repo.settings.collaboration.admin"}}" data-value="3">{{$.i18n.Tr "repo.settings.collaboration.admin"}}</div>
@@ -51,6 +51,63 @@
<button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button>
</form>
</div>
+
+ <h4 class="ui top attached header">
+ Teams
+ </h4>
+ {{ $allowedToChangeTeams := ( or (.Org.RepoAdminChangeTeamAccess) (.Permission.IsOwner)) }}
+ {{if .Teams}}
+ <div class="ui attached segment collaborator list">
+ {{range $t, $team := .Teams}}
+ <div class="item ui grid">
+ <div class="ui five wide column">
+ <a href="{{AppSubUrl}}/org/{{$.OrgName}}/teams/{{.LowerName}}">
+ {{.Name}}
+ </a>
+ </div>
+ <div class="ui eight wide column poping up" data-content="Team's permission is set on the team setting page and can't be changed per repository">
+ <span class="octicon octicon-shield"></span>
+ <div class="ui inline dropdown">
+ <div class="text">{{if eq .Authorize 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Authorize 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Authorize 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else if eq .Authorize 4}}{{$.i18n.Tr "repo.settings.collaboration.owner"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div>
+ </div>
+ {{ if or (eq .Authorize 1) (eq .Authorize 2) }}
+ {{ $first := true }}
+ <div class="description">
+ Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled $unit.Type) ($team.UnitEnabled $unit.Type)}}{{if $first}}{{ $first = false }}{{else}}, {{end}}{{$.i18n.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}}
+ </div>
+ {{end}}
+ </div>
+ {{if $allowedToChangeTeams}}
+ {{ $globalRepoAccess := (eq .LowerName "owners") }}
+ <div class="ui two wide column {{if $globalRepoAccess}}poping up{{end}}" {{if $globalRepoAccess}}data-content="This team has access to all repositories and can't be removed."{{end}}>
+ <button class="ui red tiny button inline text-thin delete-button {{if $globalRepoAccess}}disabled{{end}}" data-url="{{$.Link}}/team/delete" data-id="{{.ID}}">
+ {{$.i18n.Tr "repo.settings.delete_collaborator"}}
+ </button>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ <div class="ui bottom attached segment">
+ {{if $allowedToChangeTeams}}
+ <form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="inline field ui left">
+ <div id="search-team-box" class="ui search" data-org="{{.OrgID}}">
+ <div class="ui input">
+ <input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required>
+ </div>
+ </div>
+ </div>
+ <button class="ui green button">Add Team</button>
+ </form>
+ {{else}}
+ <div class="item">
+ Changing team access for repository has been restricted to organization owner
+ </div>
+ {{end}}
+ </div>
</div>
</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 69d100e00c..a5fef2f5e6 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -7718,6 +7718,10 @@
"type": "string",
"x-go-name": "Location"
},
+ "repo_admin_change_team_access": {
+ "type": "boolean",
+ "x-go-name": "RepoAdminChangeTeamAccess"
+ },
"username": {
"type": "string",
"x-go-name": "UserName"
@@ -8262,6 +8266,10 @@
"type": "string",
"x-go-name": "Location"
},
+ "repo_admin_change_team_access": {
+ "type": "boolean",
+ "x-go-name": "RepoAdminChangeTeamAccess"
+ },
"visibility": {
"description": "possible values are `public`, `limited` or `private`",
"type": "string",
@@ -9271,6 +9279,10 @@
"type": "string",
"x-go-name": "Location"
},
+ "repo_admin_change_team_access": {
+ "type": "boolean",
+ "x-go-name": "RepoAdminChangeTeamAccess"
+ },
"username": {
"type": "string",
"x-go-name": "UserName"