* 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 commentstags/v1.21.12.1
| @@ -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) | ||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -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_teams: 3 | |||||
| num_members: 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_teams: 1 | |||||
| num_members: 2 | |||||
| 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 | |||||
| @@ -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 | ||||
| @@ -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)) | |||||
| } | |||||
| @@ -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) | ||||
| @@ -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 | ||||
| @@ -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, | ||||
| @@ -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) | ||||
| @@ -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(u *User) error { | |||||
| func (repo *Repository) addCollaborator(e Engine, 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,18 +30,23 @@ func (repo *Repository) AddCollaborator(u *User) error { | |||||
| } | } | ||||
| collaboration.Mode = AccessModeWrite | collaboration.Mode = AccessModeWrite | ||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| if err = sess.Begin(); err != nil { | |||||
| if _, err = e.InsertOne(collaboration); err != nil { | |||||
| return err | 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 | 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() | 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(uid int64, mode AccessMode) error { | |||||
| func (repo *Repository) changeCollaborationAccessMode(e Engine, 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() | |||||
| defer sess.Close() | |||||
| if err = sess.Begin(); err != nil { | |||||
| return err | |||||
| } | |||||
| if _, err = sess. | |||||
| if _, err = e. | |||||
| 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() | ||||
| } | } | ||||
| @@ -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}) | ||||
| @@ -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}}, | |||||
| {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}}, | {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}}, | |||||
| {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}}, | {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}}, | |||||
| {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}}, | {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{}}, | ||||
| @@ -58,11 +58,12 @@ func (f *UpdateOrgSettingForm) Validate(ctx *macaron.Context, errs binding.Error | |||||
| // CreateTeamForm form for creating team | // CreateTeamForm form for creating team | ||||
| type CreateTeamForm struct { | 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 | // Validate validates the fields | ||||
| @@ -15,12 +15,13 @@ import ( | |||||
| // Organization contains organization context | // Organization contains organization context | ||||
| type Organization struct { | 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 | 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 | ||||
| @@ -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(), | ||||
| } | } | ||||
| @@ -15,7 +15,8 @@ type Team struct { | |||||
| // enum: none,read,write,admin,owner | // enum: none,read,write,admin,owner | ||||
| 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 | ||||
| @@ -27,7 +28,8 @@ type CreateTeamOption struct { | |||||
| // enum: read,write,admin | // enum: read,write,admin | ||||
| 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 | ||||
| @@ -39,5 +41,6 @@ type EditTeamOption struct { | |||||
| // enum: read,write,admin | // enum: read,write,admin | ||||
| 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"` | |||||
| } | } | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 { | |||||
| 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 | return | ||||
| } | } | ||||
| } | } | ||||
| @@ -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) | ||||
| @@ -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 | ||||
| } | } | ||||
| @@ -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> | ||||
| @@ -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> | ||||
| @@ -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}} | ||||
| @@ -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" | ||||