From 69a255defbf2747b066b2aeee66ba76cdd37104d Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Wed, 20 Nov 2019 12:27:49 +0100 Subject: [PATCH] Team permission to create repository in organization (#8312) * Add team permission setting to allow creating repo in organization. Signed-off-by: David Svantesson * Add test case for creating repo when have team creation access. Signed-off-by: David Svantesson * build error: should omit comparison to bool constant Signed-off-by: David Svantesson * Add comment on exported functions * Fix fixture consistency, fix existing unit tests * Fix boolean comparison in xorm query. * addCollaborator and changeCollaborationAccessMode separate steps More clear to use different if-cases. * Create and commit xorm session * fix * Add information of create repo permission in team sidebar * Add migration step * Clarify that repository creator will be administrator. * Fix some things after merge * Fix language text that use html * migrations file * Create repository permission -> Create repositories * fix merge * fix review comments --- integrations/api_repo_test.go | 2 ++ models/fixtures/org_user.yml | 13 +++++++++ models/fixtures/team.yml | 20 +++++++++++++ models/fixtures/team_user.yml | 12 ++++++++ models/fixtures/user.yml | 28 +++++++++++++++--- models/migrations/migrations.go | 2 ++ models/migrations/v109.go | 17 +++++++++++ models/org.go | 32 ++++++++++++++++++++ models/org_team.go | 1 + models/org_test.go | 12 ++++---- models/repo.go | 12 ++++++++ models/repo_collaboration.go | 52 ++++++++++++++++++++------------- models/user_test.go | 4 +-- models/userlist_test.go | 12 ++++---- modules/auth/org.go | 11 +++---- modules/context/org.go | 21 +++++++++---- modules/convert/convert.go | 1 + modules/structs/org_team.go | 9 ++++-- options/locale/locale_en-US.ini | 3 ++ routers/api/v1/org/team.go | 2 ++ routers/api/v1/repo/repo.go | 8 ++--- routers/org/teams.go | 2 ++ routers/repo/repo.go | 10 +++---- templates/org/home.tmpl | 2 +- templates/org/team/new.tmpl | 12 ++++++-- templates/org/team/sidebar.tmpl | 3 ++ templates/swagger/v1_json.tmpl | 12 ++++++++ 27 files changed, 252 insertions(+), 63 deletions(-) create mode 100644 models/migrations/v109.go diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index 1682f386d..e021911af 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -347,6 +347,8 @@ func TestAPIOrgRepoCreate(t *testing.T) { {ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated}, {ctxUserID: 2, orgName: "user3", repoName: "repo-own", expectedStatus: http.StatusCreated}, {ctxUserID: 2, orgName: "user6", repoName: "repo-bad-org", expectedStatus: http.StatusForbidden}, + {ctxUserID: 28, orgName: "user3", repoName: "repo-creator", expectedStatus: http.StatusCreated}, + {ctxUserID: 28, orgName: "user6", repoName: "repo-not-creator", expectedStatus: http.StatusForbidden}, } prepareTestEnv(t) diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml index 385492dd6..0b6a5e60a 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -45,3 +45,16 @@ uid: 24 org_id: 25 is_public: true + +- + id: 9 + uid: 28 + org_id: 3 + is_public: true + +- + id: 10 + uid: 28 + org_id: 6 + is_public: true + diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 4da87b731..b7e385617 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -96,3 +96,23 @@ authorize: 1 # read num_repos: 0 num_members: 0 + +- + id: 12 + org_id: 3 + lower_name: team12creators + name: team12Creators + authorize: 3 # admin + num_repos: 0 + num_members: 1 + can_create_org_repo: true + +- + id: 13 + org_id: 6 + lower_name: team13notcreators + name: team13NotCreators + authorize: 3 # admin + num_repos: 0 + num_members: 1 + can_create_org_repo: false diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 4fc609791..d541156fe 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -69,3 +69,15 @@ org_id: 25 team_id: 10 uid: 24 + +- + id: 13 + org_id: 3 + team_id: 12 + uid: 28 + +- + id: 14 + org_id: 6 + team_id: 13 + uid: 28 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 5a3b04994..17294b881 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -50,8 +50,8 @@ avatar: avatar3 avatar_email: user3@example.com num_repos: 3 - num_members: 2 - num_teams: 3 + num_members: 3 + num_teams: 4 - id: 4 @@ -102,8 +102,8 @@ avatar: avatar6 avatar_email: user6@example.com num_repos: 0 - num_members: 1 - num_teams: 1 + num_members: 2 + num_teams: 2 - id: 7 @@ -443,3 +443,23 @@ avatar: avatar27 avatar_email: user27@example.com num_repos: 2 + +- + id: 28 + lower_name: user28 + name: user28 + full_name: "user27" + email: user28@example.com + keep_email_private: true + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password + type: 0 # individual + salt: ZogKvWdyEx + is_admin: false + avatar: avatar28 + avatar_email: user28@example.com + num_repos: 0 + num_stars: 0 + num_followers: 0 + num_following: 0 + is_active: true + diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e5bfc2b88..df82fe9b8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -272,6 +272,8 @@ var migrations = []Migration{ NewMigration("Add template options to repository", addTemplateToRepo), // v108 -> v109 NewMigration("Add comment_id on table notification", addCommentIDOnNotification), + // v109 -> v110 + NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam), } // Migrate database to current version diff --git a/models/migrations/v109.go b/models/migrations/v109.go new file mode 100644 index 000000000..abe731768 --- /dev/null +++ b/models/migrations/v109.go @@ -0,0 +1,17 @@ +// 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 addCanCreateOrgRepoColumnForTeam(x *xorm.Engine) error { + type Team struct { + CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync2(new(Team)) +} diff --git a/models/org.go b/models/org.go index 78b035b10..f14dad1db 100644 --- a/models/org.go +++ b/models/org.go @@ -29,6 +29,11 @@ func (org *User) IsOrgMember(uid int64) (bool, error) { return IsOrganizationMember(org.ID, uid) } +// CanCreateOrgRepo returns true if given user can create repo in organization +func (org *User) CanCreateOrgRepo(uid int64) (bool, error) { + return CanCreateOrgRepo(org.ID, uid) +} + func (org *User) getTeam(e Engine, name string) (*Team, error) { return getTeam(e, org.ID, name) } @@ -158,6 +163,7 @@ func CreateOrganization(org, owner *User) (err error) { Authorize: AccessModeOwner, NumMembers: 1, IncludesAllRepositories: true, + CanCreateOrgRepo: true, } if _, err = sess.Insert(t); err != nil { return fmt.Errorf("insert owner team: %v", err) @@ -339,6 +345,19 @@ func IsPublicMembership(orgID, uid int64) (bool, error) { Exist() } +// CanCreateOrgRepo returns true if user can create repo in organization +func CanCreateOrgRepo(orgID, uid int64) (bool, error) { + if owner, err := IsOrganizationOwner(orgID, uid); owner || err != nil { + return owner, err + } + return x. + Where(builder.Eq{"team.can_create_org_repo": true}). + Join("INNER", "team_user", "team_user.team_id = team.id"). + And("team_user.uid = ?", uid). + And("team_user.org_id = ?", orgID). + Exist(new(Team)) +} + func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) { orgs := make([]*User, 0, 10) if !showAll { @@ -418,6 +437,19 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) { return getOwnedOrgsByUserID(x.Desc(desc), userID) } +// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID +// are allowed to create repos. +func GetOrgsCanCreateRepoByUserID(userID int64) ([]*User, error) { + orgs := make([]*User, 0, 10) + + return orgs, x.Join("INNER", "`team_user`", "`team_user`.org_id=`user`.id"). + Join("INNER", "`team`", "`team`.id=`team_user`.team_id"). + Where("`team_user`.uid=?", userID). + And(builder.Eq{"`team`.authorize": AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})). + Desc("`user`.updated_unix"). + Find(&orgs) +} + // GetOrgUsersByUserID returns all organization-user relations by user ID. func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) { ous := make([]*OrgUser, 0, 10) diff --git a/models/org_team.go b/models/org_team.go index 126a8c896..2dadf3820 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -34,6 +34,7 @@ type Team struct { NumMembers int Units []*TeamUnit `xorm:"-"` IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` + CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"` } // SearchTeamOptions holds the search options diff --git a/models/org_test.go b/models/org_test.go index 2f2c5a2d5..1a6b288dc 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -87,10 +87,11 @@ func TestUser_GetTeams(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) assert.NoError(t, org.GetTeams()) - if assert.Len(t, org.Teams, 3) { + if assert.Len(t, org.Teams, 4) { assert.Equal(t, int64(1), org.Teams[0].ID) assert.Equal(t, int64(2), org.Teams[1].ID) - assert.Equal(t, int64(7), org.Teams[2].ID) + assert.Equal(t, int64(12), org.Teams[2].ID) + assert.Equal(t, int64(7), org.Teams[3].ID) } } @@ -98,9 +99,10 @@ func TestUser_GetMembers(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) assert.NoError(t, org.GetMembers()) - if assert.Len(t, org.Members, 2) { + if assert.Len(t, org.Members, 3) { assert.Equal(t, int64(2), org.Members[0].ID) - assert.Equal(t, int64(4), org.Members[1].ID) + assert.Equal(t, int64(28), org.Members[1].ID) + assert.Equal(t, int64(4), org.Members[2].ID) } } @@ -395,7 +397,7 @@ func TestGetOrgUsersByOrgID(t *testing.T) { orgUsers, err := GetOrgUsersByOrgID(3) assert.NoError(t, err) - if assert.Len(t, orgUsers, 2) { + if assert.Len(t, orgUsers, 3) { assert.Equal(t, OrgUser{ ID: orgUsers[0].ID, OrgID: 3, diff --git a/models/repo.go b/models/repo.go index 851add409..eecc36377 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1586,6 +1586,18 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err } } } + + if isAdmin, err := isUserRepoAdmin(e, repo, doer); err != nil { + return fmt.Errorf("isUserRepoAdmin: %v", err) + } else if !isAdmin { + // Make creator repo admin if it wan't assigned automatically + if err = repo.addCollaborator(e, doer); err != nil { + return fmt.Errorf("AddCollaborator: %v", err) + } + if err = repo.changeCollaborationAccessMode(e, doer.ID, AccessModeAdmin); err != nil { + return fmt.Errorf("ChangeCollaborationAccessMode: %v", err) + } + } } else if err = repo.recalculateAccesses(e); err != nil { // Organization automatically called this in addRepository method. return fmt.Errorf("recalculateAccesses: %v", err) diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 3d6447c19..f04507f3e 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -16,14 +16,13 @@ type Collaboration struct { Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` } -// AddCollaborator adds new collaboration to a repository with default access mode. -func (repo *Repository) AddCollaborator(u *User) error { +func (repo *Repository) addCollaborator(e Engine, u *User) error { collaboration := &Collaboration{ RepoID: repo.ID, UserID: u.ID, } - has, err := x.Get(collaboration) + has, err := e.Get(collaboration) if err != nil { return err } else if has { @@ -31,18 +30,23 @@ func (repo *Repository) AddCollaborator(u *User) error { } collaboration.Mode = AccessModeWrite - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { + if _, err = e.InsertOne(collaboration); err != nil { return err } - if _, err = sess.InsertOne(collaboration); err != nil { + return repo.recalculateUserAccess(e, u.ID) +} + +// AddCollaborator adds new collaboration to a repository with default access mode. +func (repo *Repository) AddCollaborator(u *User) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { return err } - if err = repo.recalculateUserAccess(sess, u.ID); err != nil { - return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err) + if err := repo.addCollaborator(sess, u); err != nil { + return err } return sess.Commit() @@ -105,8 +109,7 @@ func (repo *Repository) IsCollaborator(userID int64) (bool, error) { return repo.isCollaborator(x, userID) } -// ChangeCollaborationAccessMode sets new access mode for the collaboration. -func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error { +func (repo *Repository) changeCollaborationAccessMode(e Engine, uid int64, mode AccessMode) error { // Discard invalid input if mode <= AccessModeNone || mode > AccessModeOwner { return nil @@ -116,7 +119,7 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode RepoID: repo.ID, UserID: uid, } - has, err := x.Get(collaboration) + has, err := e.Get(collaboration) if err != nil { return fmt.Errorf("get collaboration: %v", err) } else if !has { @@ -128,21 +131,30 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode } collaboration.Mode = mode - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess. + if _, err = e. ID(collaboration.ID). Cols("mode"). Update(collaboration); err != nil { return fmt.Errorf("update collaboration: %v", err) - } else if _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { + } else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { return fmt.Errorf("update access table: %v", err) } + return nil +} + +// ChangeCollaborationAccessMode sets new access mode for the collaboration. +func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := repo.changeCollaborationAccessMode(sess, uid, mode); err != nil { + return err + } + return sess.Commit() } diff --git a/models/user_test.go b/models/user_test.go index 2969e34a7..95f4d5d36 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -153,13 +153,13 @@ func TestSearchUsers(t *testing.T) { } testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, - []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27}) + []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28}) testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse}, []int64{9}) testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, - []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24}) + []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28}) testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) diff --git a/models/userlist_test.go b/models/userlist_test.go index ca08cc90c..c48cfb61c 100644 --- a/models/userlist_test.go +++ b/models/userlist_test.go @@ -17,8 +17,8 @@ func TestUserListIsPublicMember(t *testing.T) { orgid int64 expected map[int64]bool }{ - {3, map[int64]bool{2: true, 4: false}}, - {6, map[int64]bool{5: true}}, + {3, map[int64]bool{2: true, 4: false, 28: true}}, + {6, map[int64]bool{5: true, 28: true}}, {7, map[int64]bool{5: false}}, {25, map[int64]bool{24: true}}, {22, map[int64]bool{}}, @@ -43,8 +43,8 @@ func TestUserListIsUserOrgOwner(t *testing.T) { orgid int64 expected map[int64]bool }{ - {3, map[int64]bool{2: true, 4: false}}, - {6, map[int64]bool{5: true}}, + {3, map[int64]bool{2: true, 4: false, 28: false}}, + {6, map[int64]bool{5: true, 28: false}}, {7, map[int64]bool{5: true}}, {25, map[int64]bool{24: false}}, // ErrTeamNotExist {22, map[int64]bool{}}, // No member @@ -69,8 +69,8 @@ func TestUserListIsTwoFaEnrolled(t *testing.T) { orgid int64 expected map[int64]bool }{ - {3, map[int64]bool{2: false, 4: false}}, - {6, map[int64]bool{5: false}}, + {3, map[int64]bool{2: false, 4: false, 28: false}}, + {6, map[int64]bool{5: false, 28: false}}, {7, map[int64]bool{5: false}}, {25, map[int64]bool{24: true}}, {22, map[int64]bool{}}, diff --git a/modules/auth/org.go b/modules/auth/org.go index 509358882..20e2b0999 100644 --- a/modules/auth/org.go +++ b/modules/auth/org.go @@ -58,11 +58,12 @@ func (f *UpdateOrgSettingForm) Validate(ctx *macaron.Context, errs binding.Error // CreateTeamForm form for creating team type CreateTeamForm struct { - TeamName string `binding:"Required;AlphaDashDot;MaxSize(30)"` - Description string `binding:"MaxSize(255)"` - Permission string - Units []models.UnitType - RepoAccess string + TeamName string `binding:"Required;AlphaDashDot;MaxSize(30)"` + Description string `binding:"MaxSize(255)"` + Permission string + Units []models.UnitType + RepoAccess string + CanCreateOrgRepo bool } // Validate validates the fields diff --git a/modules/context/org.go b/modules/context/org.go index 10791c9d0..ae19aebfc 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -15,12 +15,13 @@ import ( // Organization contains organization context type Organization struct { - IsOwner bool - IsMember bool - IsTeamMember bool // Is member of team. - IsTeamAdmin bool // In owner team or team that has admin permission level. - Organization *models.User - OrgLink string + IsOwner bool + IsMember bool + IsTeamMember bool // Is member of team. + IsTeamAdmin bool // In owner team or team that has admin permission level. + Organization *models.User + OrgLink string + CanCreateOrgRepo bool Team *models.Team } @@ -73,6 +74,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Org.IsMember = true ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true + ctx.Org.CanCreateOrgRepo = true } else if ctx.IsSigned { ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID) if err != nil { @@ -84,12 +86,18 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Org.IsMember = true ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true + ctx.Org.CanCreateOrgRepo = true } else { ctx.Org.IsMember, err = org.IsOrgMember(ctx.User.ID) if err != nil { ctx.ServerError("IsOrgMember", err) return } + ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx.User.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return + } } } else { // Fake data. @@ -102,6 +110,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { } ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo ctx.Org.OrgLink = setting.AppSubURL + "/org/" + org.Name ctx.Data["OrgLink"] = ctx.Org.OrgLink diff --git a/modules/convert/convert.go b/modules/convert/convert.go index f65e4b4fe..d3b2e3816 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -249,6 +249,7 @@ func ToTeam(team *models.Team) *api.Team { Name: team.Name, Description: team.Description, IncludesAllRepositories: team.IncludesAllRepositories, + CanCreateOrgRepo: team.CanCreateOrgRepo, Permission: team.Authorize.String(), Units: team.GetUnitNames(), } diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index 5053468b4..16f83823d 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -15,7 +15,8 @@ type Team struct { // 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"] - Units []string `json:"units"` + Units []string `json:"units"` + CanCreateOrgRepo bool `json:"can_create_org_repo"` } // CreateTeamOption options for creating a team @@ -27,7 +28,8 @@ type CreateTeamOption struct { // 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"] - Units []string `json:"units"` + Units []string `json:"units"` + CanCreateOrgRepo bool `json:"can_create_org_repo"` } // EditTeamOption options for editing a team @@ -39,5 +41,6 @@ type EditTeamOption struct { // 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"] - Units []string `json:"units"` + Units []string `json:"units"` + CanCreateOrgRepo bool `json:"can_create_org_repo"` } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 99304c470..b38e909e4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1596,6 +1596,8 @@ members.invite_now = Invite Now teams.join = Join teams.leave = Leave +teams.can_create_org_repo = Create repositories +teams.can_create_org_repo_helper = Members can create new repositories in organization. Creator will get administrator access to the new repository. teams.read_access = Read Access teams.read_access_helper = Members can view and clone team repositories. teams.write_access = Write Access @@ -1615,6 +1617,7 @@ teams.delete_team_success = The team has been deleted. teams.read_permission_desc = This team grants Read access: members can view and clone team repositories. teams.write_permission_desc = This team grants Write access: members can read from and push to team repositories. teams.admin_permission_desc = This team grants Admin access: members can read from, push to and add collaborators to team repositories. +teams.create_repo_permission_desc = Additionally, this team grants Create repository permission: members can create new repositories in organization. teams.repositories = Team Repositories teams.search_repo_placeholder = Search repository… teams.remove_all_repos_title = Remove all team repositories diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index b2b5fe6da..c14742e3a 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -132,6 +132,7 @@ func CreateTeam(ctx *context.APIContext, form api.CreateTeamOption) { Name: form.Name, Description: form.Description, IncludesAllRepositories: form.IncludesAllRepositories, + CanCreateOrgRepo: form.CanCreateOrgRepo, Authorize: models.ParseAccessMode(form.Permission), } @@ -185,6 +186,7 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { team := ctx.Org.Team team.Description = form.Description unitTypes := models.FindUnitTypes(form.Units...) + team.CanCreateOrgRepo = form.CanCreateOrgRepo isAuthChanged := false isIncludeAllChanged := false diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 05ab9cb38..e2a3bfc87 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -322,12 +322,12 @@ func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) { } if !ctx.User.IsAdmin { - isOwner, err := org.IsOwnedBy(ctx.User.ID) + canCreate, err := org.CanCreateOrgRepo(ctx.User.ID) if err != nil { - ctx.ServerError("IsOwnedBy", err) + ctx.ServerError("CanCreateOrgRepo", err) return - } else if !isOwner { - ctx.Error(403, "", "Given user is not owner of organization.") + } else if !canCreate { + ctx.Error(403, "", "Given user is not allowed to create repository in organization.") return } } diff --git a/routers/org/teams.go b/routers/org/teams.go index 873265803..2aa69f5e9 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -201,6 +201,7 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) { Description: form.Description, Authorize: models.ParseAccessMode(form.Permission), IncludesAllRepositories: includesAllRepositories, + CanCreateOrgRepo: form.CanCreateOrgRepo, } if t.Authorize < models.AccessModeOwner { @@ -316,6 +317,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { return } } + t.CanCreateOrgRepo = form.CanCreateOrgRepo if ctx.HasError() { ctx.HTML(200, tplTeamNew) diff --git a/routers/repo/repo.go b/routers/repo/repo.go index cb4e48333..b78dd5376 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -53,9 +53,9 @@ func MustBeAbleToUpload(ctx *context.Context) { } func checkContextUser(ctx *context.Context, uid int64) *models.User { - orgs, err := models.GetOwnedOrgsByUserIDDesc(ctx.User.ID, "updated_unix") + orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID) if err != nil { - ctx.ServerError("GetOwnedOrgsByUserIDDesc", err) + ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) return nil } ctx.Data["Orgs"] = orgs @@ -81,11 +81,11 @@ func checkContextUser(ctx *context.Context, uid int64) *models.User { return nil } if !ctx.User.IsAdmin { - isOwner, err := org.IsOwnedBy(ctx.User.ID) + canCreate, err := org.CanCreateOrgRepo(ctx.User.ID) if err != nil { - ctx.ServerError("IsOwnedBy", err) + ctx.ServerError("CanCreateOrgRepo", err) return nil - } else if !isOwner { + } else if !canCreate { ctx.Error(403) return nil } diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 03bb52527..0aa575707 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -22,7 +22,7 @@
- {{if .IsOrganizationOwner}} + {{if .CanCreateOrgRepo}} diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index e50a1777d..c38fa4d94 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -31,14 +31,22 @@
- {{.i18n.Tr "org.teams.specific_repositories_helper"}} + {{.i18n.Tr "org.teams.specific_repositories_helper" | Str2html}}
- {{.i18n.Tr "org.teams.all_repositories_helper"}} + {{.i18n.Tr "org.teams.all_repositories_helper" | Str2html}} +
+
+ +
+
+ + + {{.i18n.Tr "org.teams.can_create_org_repo_helper"}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index dd189df5f..75c5ce756 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -40,6 +40,9 @@ {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} {{end}} {{end}} + {{if .Team.CanCreateOrgRepo}} +

{{.i18n.Tr "org.teams.create_repo_permission_desc" | Str2html}} + {{end}}
{{if .IsOrganizationOwner}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6b424131c..4427c2874 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -8279,6 +8279,10 @@ "name" ], "properties": { + "can_create_org_repo": { + "type": "boolean", + "x-go-name": "CanCreateOrgRepo" + }, "description": { "type": "string", "x-go-name": "Description" @@ -8847,6 +8851,10 @@ "name" ], "properties": { + "can_create_org_repo": { + "type": "boolean", + "x-go-name": "CanCreateOrgRepo" + }, "description": { "type": "string", "x-go-name": "Description" @@ -10506,6 +10514,10 @@ "description": "Team represents a team in an organization", "type": "object", "properties": { + "can_create_org_repo": { + "type": "boolean", + "x-go-name": "CanCreateOrgRepo" + }, "description": { "type": "string", "x-go-name": "Description"