]> source.dussan.org Git - gitea.git/commitdiff
Team permission to create repository in organization (#8312)
authorDavid Svantesson <davidsvantesson@gmail.com>
Wed, 20 Nov 2019 11:27:49 +0000 (12:27 +0100)
committerLunny Xiao <xiaolunwen@gmail.com>
Wed, 20 Nov 2019 11:27:49 +0000 (19:27 +0800)
* 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

27 files changed:
integrations/api_repo_test.go
models/fixtures/org_user.yml
models/fixtures/team.yml
models/fixtures/team_user.yml
models/fixtures/user.yml
models/migrations/migrations.go
models/migrations/v109.go [new file with mode: 0644]
models/org.go
models/org_team.go
models/org_test.go
models/repo.go
models/repo_collaboration.go
models/user_test.go
models/userlist_test.go
modules/auth/org.go
modules/context/org.go
modules/convert/convert.go
modules/structs/org_team.go
options/locale/locale_en-US.ini
routers/api/v1/org/team.go
routers/api/v1/repo/repo.go
routers/org/teams.go
routers/repo/repo.go
templates/org/home.tmpl
templates/org/team/new.tmpl
templates/org/team/sidebar.tmpl
templates/swagger/v1_json.tmpl

index 1682f386d31e9855db42b26e9e0cd7d22bf301f3..e021911afdb2dc503cc40581f95dcda81e081828 100644 (file)
@@ -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)
index 385492dd68d11f000cd308fc7547ed6d49636078..0b6a5e60a70fe5969d6cae2e49b226f4ede0d11d 100644 (file)
   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
+
index 4da87b731fff09cd894f96edd61f8a8423b8ff73..b7e38561720e2c2bd00aa8f761cc3e580f2c9c02 100644 (file)
   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
index 4fc609791d3962150479ee4b836d6d4d699c73ab..d541156fe826f6e9f21156388e765e6401f6c0e3 100644 (file)
   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
index 5a3b04994cc99cadd2ae8edd7f39754b47a32995..17294b881f08db4a8de1fd3af1104fd7f47517e1 100644 (file)
@@ -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
   avatar: avatar6
   avatar_email: user6@example.com
   num_repos: 0
-  num_members: 1
-  num_teams: 1
+  num_members: 2
+  num_teams: 2
 
 -
   id: 7
   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
+
index e5bfc2b881eaeb0c550705909f100aa8201ef6cd..df82fe9b8b1f67ef13c269505cd750030f1fc921 100644 (file)
@@ -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 (file)
index 0000000..abe7317
--- /dev/null
@@ -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))
+}
index 78b035b10147b30064f4fd4c8c6a36a8de982e74..f14dad1dbb625e50984f6da70f1f2a3d4ee09870 100644 (file)
@@ -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)
index 126a8c896a2129860f9062b7efc2a84a92079fca..2dadf3820c294464193459d51a411cf3578cddae 100644 (file)
@@ -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
index 2f2c5a2d5eb196d811c67d2b221f01b4d6db76aa..1a6b288dc75dd2bc840fccb3e98df29eb1740097 100644 (file)
@@ -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,
index 851add409f3721ff2db54cc9738670929bfab3d9..eecc36377b354652cc20e723197ff48827231a3e 100644 (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 {
                // Organization automatically called this in addRepository method.
                return fmt.Errorf("recalculateAccesses: %v", err)
index 3d6447c1963b31bbadf0d41c2c40df5cfe5e8a15..f04507f3e830c8d228205667dc8f34ab450b59a0 100644 (file)
@@ -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()
 }
 
index 2969e34a76ece06bdc07dd904ee9fc9285b6b372..95f4d5d3633bb4dc1a228fd4dd34e371a3106b43 100644 (file)
@@ -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})
index ca08cc90ce89c43c9efc1ec18c205a6a9ab3060a..c48cfb61c168b435ac2e8b0959598a9f2becf8e2 100644 (file)
@@ -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{}},
index 509358882a3852c4b828a0f7f4afbd93754f75db..20e2b09997e203e2ca860d98361ee459f9c5b7b2 100644 (file)
@@ -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
index 10791c9d010615f0b0c35b200f4950670e9a538d..ae19aebfcc6c01581a370cb3a160ec795d30f437 100644 (file)
@@ -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
index f65e4b4fe24099f033b1ba058b3ca0f2c7703677..d3b2e38165b95ce2d2deb6ba5ea4291c2de5a19c 100644 (file)
@@ -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(),
        }
index 5053468b4a53d2a1a71170a3680437af2c3d2c93..16f83823d66b5f14a09d55a11adb232ff28c969e 100644 (file)
@@ -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"`
 }
index 99304c470d39dfc73b88489282b11326397f5168..b38e909e48881847f5a3078f45919b414f268874 100644 (file)
@@ -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 <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.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.search_repo_placeholder = Search repository…
 teams.remove_all_repos_title = Remove all team repositories
index b2b5fe6dadb389e17f2dc14596da9e680155104a..c14742e3a4e640017ee7cb43c8860ba3070b6880 100644 (file)
@@ -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
index 05ab9cb38b174f480bf6c722a7d25d735d957058..e2a3bfc8730182afcf5557be7ba2741124951d5a 100644 (file)
@@ -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
                }
        }
index 873265803fd43122b6fce1d5aa2fb326db76f093..2aa69f5e93d45f7db9ce48278a31536fa284e4bc 100644 (file)
@@ -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)
index cb4e48333300557b1dc183a0f4a7408f48e0efb8..b78dd5376ee070a400ad82a4c7cdee79a7fbf448 100644 (file)
@@ -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
                }
index 03bb5252767f74b5ba713e5462d952454966a92d..0aa575707ab3c4bdcd6775f8a131fefb222b993b 100644 (file)
@@ -22,7 +22,7 @@
        <div class="ui container">
                <div class="ui mobile reversed stackable grid">
                        <div class="ui eleven wide column">
-                               {{if .IsOrganizationOwner}}
+                               {{if .CanCreateOrgRepo}}
                                        <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>
                                        </div>
index e50a1777d2002898b51bb0f4c6d6c70fc97a170f..c38fa4d94019a50cf4073f12c17d8ec1aa43ccb3 100644 (file)
                                                                <div class="ui radio checkbox">
                                                                        <input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
                                                                        <label>{{.i18n.Tr "org.teams.specific_repositories"}}</label>
-                                                                       <span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper"}}</span>
+                                                                       <span class="help">{{.i18n.Tr "org.teams.specific_repositories_helper" | Str2html}}</span>
                                                                </div>
                                                        </div>
                                                        <div class="field">
                                                                <div class="ui radio checkbox">
                                                                        <input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
                                                                        <label>{{.i18n.Tr "org.teams.all_repositories"}}</label>
-                                                                       <span class="help">{{.i18n.Tr "org.teams.all_repositories_helper"}}</span>
+                                                                       <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>
index dd189df5f313b585131ee58a1b7ee2ac3b18399e..75c5ce756d445f1dbb30cc3c2f5373aa75bdf4ab 100644 (file)
@@ -40,6 +40,9 @@
                                        {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}}
                                {{end}}
                        {{end}}
+                       {{if .Team.CanCreateOrgRepo}}
+                               <br><br>{{.i18n.Tr "org.teams.create_repo_permission_desc" | Str2html}}
+                       {{end}}
                </div>
        </div>
        {{if .IsOrganizationOwner}}
index 6b424131c5d48fa4991dc0603fa160d4c5a4d2b1..4427c28747366758ec02deda5c224920bf71da16 100644 (file)
         "name"
       ],
       "properties": {
+        "can_create_org_repo": {
+          "type": "boolean",
+          "x-go-name": "CanCreateOrgRepo"
+        },
         "description": {
           "type": "string",
           "x-go-name": "Description"
         "name"
       ],
       "properties": {
+        "can_create_org_repo": {
+          "type": "boolean",
+          "x-go-name": "CanCreateOrgRepo"
+        },
         "description": {
           "type": "string",
           "x-go-name": "Description"
       "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"