From a0e88dfc2e5cc811facf8f96d0c6ca22dc49b9e1 Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Mon, 23 Sep 2019 22:08:03 +0200 Subject: [PATCH] Add teams to repo on collaboration page. (#8045) * Add teams to repo on collaboration page. Signed-off-by: David Svantesson * Add option for repository admins to change teams access to repo. Signed-off-by: David Svantesson * Add comment for functions Signed-off-by: David Svantesson * Make RepoAdminChangeTeamAccess default false in xorm and make it default checked in template instead. Signed-off-by: David Svantesson * Make proper language strings and fix error redirection. * Add unit tests for adding and deleting team from repository. Signed-off-by: David Svantesson * Add database migration Signed-off-by: David Svantesson * Fix redirect Signed-off-by: David Svantesson * Fix locale string mismatch. Signed-off-by: David Svantesson * Move team access mode text logic to template. * Move collaborator access mode text logic to template. --- models/error.go | 17 ++ models/fixtures/repository.yml | 11 ++ models/fixtures/team.yml | 9 + models/fixtures/user.yml | 18 ++ models/migrations/migrations.go | 2 + models/migrations/v97.go | 15 ++ models/org.go | 8 +- models/org_team.go | 4 +- models/org_test.go | 6 +- models/repo_collaboration.go | 28 +-- models/repo_collaboration_test.go | 11 -- models/user.go | 13 +- models/user_test.go | 5 +- models/userlist.go | 2 +- modules/auth/org.go | 15 +- modules/structs/org.go | 23 +-- options/locale/locale_en-US.ini | 9 + public/css/index.css | 3 + public/js/index.js | 25 +++ public/less/_repository.less | 23 +++ routers/api/v1/convert/convert.go | 17 +- routers/api/v1/org/org.go | 17 +- routers/org/setting.go | 1 + routers/repo/setting.go | 83 +++++++++ routers/repo/settings_test.go | 193 +++++++++++++++++++++ routers/routes/routes.go | 4 + templates/org/create.tmpl | 11 ++ templates/org/settings/options.tmpl | 10 ++ templates/repo/settings/collaboration.tmpl | 59 ++++++- templates/swagger/v1_json.tmpl | 12 ++ 30 files changed, 575 insertions(+), 79 deletions(-) create mode 100644 models/migrations/v97.go diff --git a/models/error.go b/models/error.go index cecd03f99..c025437c5 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 e1370aa09..2e38c5e1d 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 b7265ec49..4da87b731 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 5177173e7..a204241f9 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 8717da789..7680e7747 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 000000000..fa542f2cc --- /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 e00bef579..ca3bce81a 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 799716679..90a089417 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 6c1c4fee0..2f2c5a2d5 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 0797f5043..40ddf6a28 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 f11f3c54c..084221246 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 af4ccacf6..5e87473e8 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 d01b482ae..bcb955817 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 43838a680..a2a424848 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 367468e58..2abffdf74 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 08ab13997..4b79a4e70 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 52ccea68b..7bb453968 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 webhooks guide. @@ -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 8f24e7b3e..1da2399c4 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 d99457514..ad5e3912d 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 4823d1000..fde11f7a4 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 40e4ca7ae..e0e7f609c 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 3adc204d3..8a1a478ba 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 1d534ec55..7de784c5b 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 3dc5a1e09..91db519d6 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 cf7ed840a..a05a96cea 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 2afd0dcce..93ce220b0 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 5b6080c22..8ae0fc22a 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -32,6 +32,17 @@ + +
+ +
+
+ + +
+
+
+
+
+ +
+
+ + +
+
+
+ {{if .SignedUser.IsAdmin}}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index 4c3c1a5d7..b65fd4e16 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -20,7 +20,7 @@
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 69d100e00..a5fef2f5e 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"