Team permission to create repository in organization (#8312)

* Add team permission setting to allow creating repo in organization.

Signed-off-by: David Svantesson <davidsvantesson@gmail.com>

* Add test case for creating repo when have team creation access.

Signed-off-by: David Svantesson <davidsvantesson@gmail.com>

* build error: should omit comparison to bool constant

Signed-off-by: David Svantesson <davidsvantesson@gmail.com>

* 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
This commit is contained in:
David Svantesson 2019-11-20 12:27:49 +01:00 committed by Lunny Xiao
parent 35c3ea952a
commit 69a255defb
27 changed files with 252 additions and 63 deletions

View File

@ -347,6 +347,8 @@ func TestAPIOrgRepoCreate(t *testing.T) {
{ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated}, {ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated},
{ctxUserID: 2, orgName: "user3", repoName: "repo-own", 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: 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) prepareTestEnv(t)

View File

@ -45,3 +45,16 @@
uid: 24 uid: 24
org_id: 25 org_id: 25
is_public: true is_public: true
-
id: 9
uid: 28
org_id: 3
is_public: true
-
id: 10
uid: 28
org_id: 6
is_public: true

View File

@ -96,3 +96,23 @@
authorize: 1 # read authorize: 1 # read
num_repos: 0 num_repos: 0
num_members: 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

View File

@ -69,3 +69,15 @@
org_id: 25 org_id: 25
team_id: 10 team_id: 10
uid: 24 uid: 24
-
id: 13
org_id: 3
team_id: 12
uid: 28
-
id: 14
org_id: 6
team_id: 13
uid: 28

View File

@ -50,8 +50,8 @@
avatar: avatar3 avatar: avatar3
avatar_email: user3@example.com avatar_email: user3@example.com
num_repos: 3 num_repos: 3
num_members: 2 num_members: 3
num_teams: 3 num_teams: 4
- -
id: 4 id: 4
@ -102,8 +102,8 @@
avatar: avatar6 avatar: avatar6
avatar_email: user6@example.com avatar_email: user6@example.com
num_repos: 0 num_repos: 0
num_members: 1 num_members: 2
num_teams: 1 num_teams: 2
- -
id: 7 id: 7
@ -443,3 +443,23 @@
avatar: avatar27 avatar: avatar27
avatar_email: user27@example.com avatar_email: user27@example.com
num_repos: 2 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

View File

@ -272,6 +272,8 @@ var migrations = []Migration{
NewMigration("Add template options to repository", addTemplateToRepo), NewMigration("Add template options to repository", addTemplateToRepo),
// v108 -> v109 // v108 -> v109
NewMigration("Add comment_id on table notification", addCommentIDOnNotification), NewMigration("Add comment_id on table notification", addCommentIDOnNotification),
// v109 -> v110
NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
} }
// Migrate database to current version // Migrate database to current version

17
models/migrations/v109.go Normal file
View File

@ -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))
}

View File

@ -29,6 +29,11 @@ func (org *User) IsOrgMember(uid int64) (bool, error) {
return IsOrganizationMember(org.ID, uid) 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) { func (org *User) getTeam(e Engine, name string) (*Team, error) {
return getTeam(e, org.ID, name) return getTeam(e, org.ID, name)
} }
@ -158,6 +163,7 @@ func CreateOrganization(org, owner *User) (err error) {
Authorize: AccessModeOwner, Authorize: AccessModeOwner,
NumMembers: 1, NumMembers: 1,
IncludesAllRepositories: true, IncludesAllRepositories: true,
CanCreateOrgRepo: true,
} }
if _, err = sess.Insert(t); err != nil { if _, err = sess.Insert(t); err != nil {
return fmt.Errorf("insert owner team: %v", err) return fmt.Errorf("insert owner team: %v", err)
@ -339,6 +345,19 @@ func IsPublicMembership(orgID, uid int64) (bool, error) {
Exist() 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) { func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) {
orgs := make([]*User, 0, 10) orgs := make([]*User, 0, 10)
if !showAll { if !showAll {
@ -418,6 +437,19 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
return getOwnedOrgsByUserID(x.Desc(desc), userID) 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. // GetOrgUsersByUserID returns all organization-user relations by user ID.
func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) { func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) {
ous := make([]*OrgUser, 0, 10) ous := make([]*OrgUser, 0, 10)

View File

@ -34,6 +34,7 @@ type Team struct {
NumMembers int NumMembers int
Units []*TeamUnit `xorm:"-"` Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
} }
// SearchTeamOptions holds the search options // SearchTeamOptions holds the search options

View File

@ -87,10 +87,11 @@ func TestUser_GetTeams(t *testing.T) {
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
assert.NoError(t, org.GetTeams()) 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(1), org.Teams[0].ID)
assert.Equal(t, int64(2), org.Teams[1].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()) assert.NoError(t, PrepareTestDatabase())
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
assert.NoError(t, org.GetMembers()) 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(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) orgUsers, err := GetOrgUsersByOrgID(3)
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, orgUsers, 2) { if assert.Len(t, orgUsers, 3) {
assert.Equal(t, OrgUser{ assert.Equal(t, OrgUser{
ID: orgUsers[0].ID, ID: orgUsers[0].ID,
OrgID: 3, OrgID: 3,

View File

@ -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 { } else if err = repo.recalculateAccesses(e); err != nil {
// Organization automatically called this in addRepository method. // Organization automatically called this in addRepository method.
return fmt.Errorf("recalculateAccesses: %v", err) return fmt.Errorf("recalculateAccesses: %v", err)

View File

@ -16,14 +16,13 @@ type Collaboration struct {
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
} }
// AddCollaborator adds new collaboration to a repository with default access mode. func (repo *Repository) addCollaborator(e Engine, u *User) error {
func (repo *Repository) AddCollaborator(u *User) error {
collaboration := &Collaboration{ collaboration := &Collaboration{
RepoID: repo.ID, RepoID: repo.ID,
UserID: u.ID, UserID: u.ID,
} }
has, err := x.Get(collaboration) has, err := e.Get(collaboration)
if err != nil { if err != nil {
return err return err
} else if has { } else if has {
@ -31,20 +30,25 @@ func (repo *Repository) AddCollaborator(u *User) error {
} }
collaboration.Mode = AccessModeWrite collaboration.Mode = AccessModeWrite
if _, err = e.InsertOne(collaboration); err != nil {
return err
}
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() sess := x.NewSession()
defer sess.Close() defer sess.Close()
if err = sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
return err return err
} }
if _, err = sess.InsertOne(collaboration); err != nil { if err := repo.addCollaborator(sess, u); err != nil {
return err return err
} }
if err = repo.recalculateUserAccess(sess, u.ID); err != nil {
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err)
}
return sess.Commit() return sess.Commit()
} }
@ -105,8 +109,7 @@ func (repo *Repository) IsCollaborator(userID int64) (bool, error) {
return repo.isCollaborator(x, userID) return repo.isCollaborator(x, userID)
} }
// ChangeCollaborationAccessMode sets new access mode for the collaboration. func (repo *Repository) changeCollaborationAccessMode(e Engine, uid int64, mode AccessMode) error {
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error {
// Discard invalid input // Discard invalid input
if mode <= AccessModeNone || mode > AccessModeOwner { if mode <= AccessModeNone || mode > AccessModeOwner {
return nil return nil
@ -116,7 +119,7 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode
RepoID: repo.ID, RepoID: repo.ID,
UserID: uid, UserID: uid,
} }
has, err := x.Get(collaboration) has, err := e.Get(collaboration)
if err != nil { if err != nil {
return fmt.Errorf("get collaboration: %v", err) return fmt.Errorf("get collaboration: %v", err)
} else if !has { } else if !has {
@ -128,21 +131,30 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode
} }
collaboration.Mode = mode collaboration.Mode = mode
sess := x.NewSession() if _, err = e.
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.
ID(collaboration.ID). ID(collaboration.ID).
Cols("mode"). Cols("mode").
Update(collaboration); err != nil { Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %v", err) 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 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() return sess.Commit()
} }

View File

@ -153,13 +153,13 @@ func TestSearchUsers(t *testing.T) {
} }
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, 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}, testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
[]int64{9}) []int64{9})
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, 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}, testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})

View File

@ -17,8 +17,8 @@ func TestUserListIsPublicMember(t *testing.T) {
orgid int64 orgid int64
expected map[int64]bool expected map[int64]bool
}{ }{
{3, map[int64]bool{2: true, 4: false}}, {3, map[int64]bool{2: true, 4: false, 28: true}},
{6, map[int64]bool{5: true}}, {6, map[int64]bool{5: true, 28: true}},
{7, map[int64]bool{5: false}}, {7, map[int64]bool{5: false}},
{25, map[int64]bool{24: true}}, {25, map[int64]bool{24: true}},
{22, map[int64]bool{}}, {22, map[int64]bool{}},
@ -43,8 +43,8 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
orgid int64 orgid int64
expected map[int64]bool expected map[int64]bool
}{ }{
{3, map[int64]bool{2: true, 4: false}}, {3, map[int64]bool{2: true, 4: false, 28: false}},
{6, map[int64]bool{5: true}}, {6, map[int64]bool{5: true, 28: false}},
{7, map[int64]bool{5: true}}, {7, map[int64]bool{5: true}},
{25, map[int64]bool{24: false}}, // ErrTeamNotExist {25, map[int64]bool{24: false}}, // ErrTeamNotExist
{22, map[int64]bool{}}, // No member {22, map[int64]bool{}}, // No member
@ -69,8 +69,8 @@ func TestUserListIsTwoFaEnrolled(t *testing.T) {
orgid int64 orgid int64
expected map[int64]bool expected map[int64]bool
}{ }{
{3, map[int64]bool{2: false, 4: false}}, {3, map[int64]bool{2: false, 4: false, 28: false}},
{6, map[int64]bool{5: false}}, {6, map[int64]bool{5: false, 28: false}},
{7, map[int64]bool{5: false}}, {7, map[int64]bool{5: false}},
{25, map[int64]bool{24: true}}, {25, map[int64]bool{24: true}},
{22, map[int64]bool{}}, {22, map[int64]bool{}},

View File

@ -63,6 +63,7 @@ type CreateTeamForm struct {
Permission string Permission string
Units []models.UnitType Units []models.UnitType
RepoAccess string RepoAccess string
CanCreateOrgRepo bool
} }
// Validate validates the fields // Validate validates the fields

View File

@ -21,6 +21,7 @@ type Organization struct {
IsTeamAdmin bool // In owner team or team that has admin permission level. IsTeamAdmin bool // In owner team or team that has admin permission level.
Organization *models.User Organization *models.User
OrgLink string OrgLink string
CanCreateOrgRepo bool
Team *models.Team Team *models.Team
} }
@ -73,6 +74,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Org.IsMember = true ctx.Org.IsMember = true
ctx.Org.IsTeamMember = true ctx.Org.IsTeamMember = true
ctx.Org.IsTeamAdmin = true ctx.Org.IsTeamAdmin = true
ctx.Org.CanCreateOrgRepo = true
} else if ctx.IsSigned { } else if ctx.IsSigned {
ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID) ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
if err != nil { if err != nil {
@ -84,12 +86,18 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Org.IsMember = true ctx.Org.IsMember = true
ctx.Org.IsTeamMember = true ctx.Org.IsTeamMember = true
ctx.Org.IsTeamAdmin = true ctx.Org.IsTeamAdmin = true
ctx.Org.CanCreateOrgRepo = true
} else { } else {
ctx.Org.IsMember, err = org.IsOrgMember(ctx.User.ID) ctx.Org.IsMember, err = org.IsOrgMember(ctx.User.ID)
if err != nil { if err != nil {
ctx.ServerError("IsOrgMember", err) ctx.ServerError("IsOrgMember", err)
return return
} }
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx.User.ID)
if err != nil {
ctx.ServerError("CanCreateOrgRepo", err)
return
}
} }
} else { } else {
// Fake data. // Fake data.
@ -102,6 +110,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
} }
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + org.Name ctx.Org.OrgLink = setting.AppSubURL + "/org/" + org.Name
ctx.Data["OrgLink"] = ctx.Org.OrgLink ctx.Data["OrgLink"] = ctx.Org.OrgLink

View File

@ -249,6 +249,7 @@ func ToTeam(team *models.Team) *api.Team {
Name: team.Name, Name: team.Name,
Description: team.Description, Description: team.Description,
IncludesAllRepositories: team.IncludesAllRepositories, IncludesAllRepositories: team.IncludesAllRepositories,
CanCreateOrgRepo: team.CanCreateOrgRepo,
Permission: team.Authorize.String(), Permission: team.Authorize.String(),
Units: team.GetUnitNames(), Units: team.GetUnitNames(),
} }

View File

@ -16,6 +16,7 @@ type Team struct {
Permission string `json:"permission"` Permission string `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] // 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 // CreateTeamOption options for creating a team
@ -28,6 +29,7 @@ type CreateTeamOption struct {
Permission string `json:"permission"` Permission string `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] // 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 // EditTeamOption options for editing a team
@ -40,4 +42,5 @@ type EditTeamOption struct {
Permission string `json:"permission"` Permission string `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] // 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"`
} }

View File

@ -1596,6 +1596,8 @@ members.invite_now = Invite Now
teams.join = Join teams.join = Join
teams.leave = Leave 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 = Read Access
teams.read_access_helper = Members can view and clone team repositories. teams.read_access_helper = Members can view and clone team repositories.
teams.write_access = Write Access 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 <strong>Read</strong> access: members can view and clone team repositories. teams.read_permission_desc = This team grants <strong>Read</strong> access: members can view and clone team repositories.
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories. teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories.
teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories. teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories.
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
teams.repositories = Team Repositories teams.repositories = Team Repositories
teams.search_repo_placeholder = Search repository… teams.search_repo_placeholder = Search repository…
teams.remove_all_repos_title = Remove all team repositories teams.remove_all_repos_title = Remove all team repositories

View File

@ -132,6 +132,7 @@ func CreateTeam(ctx *context.APIContext, form api.CreateTeamOption) {
Name: form.Name, Name: form.Name,
Description: form.Description, Description: form.Description,
IncludesAllRepositories: form.IncludesAllRepositories, IncludesAllRepositories: form.IncludesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
Authorize: models.ParseAccessMode(form.Permission), Authorize: models.ParseAccessMode(form.Permission),
} }
@ -185,6 +186,7 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) {
team := ctx.Org.Team team := ctx.Org.Team
team.Description = form.Description team.Description = form.Description
unitTypes := models.FindUnitTypes(form.Units...) unitTypes := models.FindUnitTypes(form.Units...)
team.CanCreateOrgRepo = form.CanCreateOrgRepo
isAuthChanged := false isAuthChanged := false
isIncludeAllChanged := false isIncludeAllChanged := false

View File

@ -322,12 +322,12 @@ func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
} }
if !ctx.User.IsAdmin { if !ctx.User.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx.User.ID) canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
if err != nil { if err != nil {
ctx.ServerError("IsOwnedBy", err) ctx.ServerError("CanCreateOrgRepo", err)
return return
} else if !isOwner { } else if !canCreate {
ctx.Error(403, "", "Given user is not owner of organization.") ctx.Error(403, "", "Given user is not allowed to create repository in organization.")
return return
} }
} }

View File

@ -201,6 +201,7 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
Description: form.Description, Description: form.Description,
Authorize: models.ParseAccessMode(form.Permission), Authorize: models.ParseAccessMode(form.Permission),
IncludesAllRepositories: includesAllRepositories, IncludesAllRepositories: includesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
} }
if t.Authorize < models.AccessModeOwner { if t.Authorize < models.AccessModeOwner {
@ -316,6 +317,7 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) {
return return
} }
} }
t.CanCreateOrgRepo = form.CanCreateOrgRepo
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(200, tplTeamNew) ctx.HTML(200, tplTeamNew)

View File

@ -53,9 +53,9 @@ func MustBeAbleToUpload(ctx *context.Context) {
} }
func checkContextUser(ctx *context.Context, uid int64) *models.User { 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 { if err != nil {
ctx.ServerError("GetOwnedOrgsByUserIDDesc", err) ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
return nil return nil
} }
ctx.Data["Orgs"] = orgs ctx.Data["Orgs"] = orgs
@ -81,11 +81,11 @@ func checkContextUser(ctx *context.Context, uid int64) *models.User {
return nil return nil
} }
if !ctx.User.IsAdmin { if !ctx.User.IsAdmin {
isOwner, err := org.IsOwnedBy(ctx.User.ID) canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
if err != nil { if err != nil {
ctx.ServerError("IsOwnedBy", err) ctx.ServerError("CanCreateOrgRepo", err)
return nil return nil
} else if !isOwner { } else if !canCreate {
ctx.Error(403) ctx.Error(403)
return nil return nil
} }

View File

@ -22,7 +22,7 @@
<div class="ui container"> <div class="ui container">
<div class="ui mobile reversed stackable grid"> <div class="ui mobile reversed stackable grid">
<div class="ui eleven wide column"> <div class="ui eleven wide column">
{{if .IsOrganizationOwner}} {{if .CanCreateOrgRepo}}
<div class="text right"> <div class="text right">
<a class="ui green button" href="{{AppSubUrl}}/repo/create?org={{.Org.ID}}"><i class="octicon octicon-repo-create"></i> {{.i18n.Tr "new_repo"}}</a> <a class="ui green button" href="{{AppSubUrl}}/repo/create?org={{.Org.ID}}"><i class="octicon octicon-repo-create"></i> {{.i18n.Tr "new_repo"}}</a>
</div> </div>

View File

@ -31,14 +31,22 @@
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}> <input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
<label>{{.i18n.Tr "org.teams.specific_repositories"}}</label> <label>{{.i18n.Tr "org.teams.specific_repositories"}}</label>
<span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper"}}</span> <span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper" | Str2html}}</span>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}> <input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
<label>{{.i18n.Tr "org.teams.all_repositories"}}</label> <label>{{.i18n.Tr "org.teams.all_repositories"}}</label>
<span class="help">{{.i18n.Tr "org.teams.all_repositories_helper"}}</span> <span class="help">{{.i18n.Tr "org.teams.all_repositories_helper" | Str2html}}</span>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="can_create_org_repo">{{.i18n.Tr "org.teams.can_create_org_repo"}}</label>
<input id="can_create_org_repo" name="can_create_org_repo" type="checkbox" {{if .Team.CanCreateOrgRepo}}checked{{end}}>
<span class="help">{{.i18n.Tr "org.teams.can_create_org_repo_helper"}}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,6 +40,9 @@
{{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}}
{{end}} {{end}}
{{end}} {{end}}
{{if .Team.CanCreateOrgRepo}}
<br><br>{{.i18n.Tr "org.teams.create_repo_permission_desc" | Str2html}}
{{end}}
</div> </div>
</div> </div>
{{if .IsOrganizationOwner}} {{if .IsOrganizationOwner}}

View File

@ -8279,6 +8279,10 @@
"name" "name"
], ],
"properties": { "properties": {
"can_create_org_repo": {
"type": "boolean",
"x-go-name": "CanCreateOrgRepo"
},
"description": { "description": {
"type": "string", "type": "string",
"x-go-name": "Description" "x-go-name": "Description"
@ -8847,6 +8851,10 @@
"name" "name"
], ],
"properties": { "properties": {
"can_create_org_repo": {
"type": "boolean",
"x-go-name": "CanCreateOrgRepo"
},
"description": { "description": {
"type": "string", "type": "string",
"x-go-name": "Description" "x-go-name": "Description"
@ -10506,6 +10514,10 @@
"description": "Team represents a team in an organization", "description": "Team represents a team in an organization",
"type": "object", "type": "object",
"properties": { "properties": {
"can_create_org_repo": {
"type": "boolean",
"x-go-name": "CanCreateOrgRepo"
},
"description": { "description": {
"type": "string", "type": "string",
"x-go-name": "Description" "x-go-name": "Description"