Skip to content

Commit

Permalink
Team permission to create repository in organization (#8312)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
davidsvantesson authored and lunny committed Nov 20, 2019
1 parent 35c3ea9 commit 69a255d
Show file tree
Hide file tree
Showing 27 changed files with 252 additions and 63 deletions.
2 changes: 2 additions & 0 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions models/fixtures/org_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,16 @@
uid: 24
org_id: 25
is_public: true

-
id: 9
uid: 28
org_id: 3
is_public: true

-
id: 10
uid: 28
org_id: 6
is_public: true

20 changes: 20 additions & 0 deletions models/fixtures/team.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,23 @@
authorize: 1 # read
num_repos: 0
num_members: 0

-
id: 12
org_id: 3
lower_name: team12creators
name: team12Creators
authorize: 3 # admin
num_repos: 0
num_members: 1
can_create_org_repo: true

-
id: 13
org_id: 6
lower_name: team13notcreators
name: team13NotCreators
authorize: 3 # admin
num_repos: 0
num_members: 1
can_create_org_repo: false
12 changes: 12 additions & 0 deletions models/fixtures/team_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,15 @@
org_id: 25
team_id: 10
uid: 24

-
id: 13
org_id: 3
team_id: 12
uid: 28

-
id: 14
org_id: 6
team_id: 13
uid: 28
28 changes: 24 additions & 4 deletions models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -102,8 +102,8 @@
avatar: avatar6
avatar_email: user6@example.com
num_repos: 0
num_members: 1
num_teams: 1
num_members: 2
num_teams: 2

-
id: 7
Expand Down Expand Up @@ -443,3 +443,23 @@
avatar: avatar27
avatar_email: user27@example.com
num_repos: 2

-
id: 28
lower_name: user28
name: user28
full_name: "user27"
email: user28@example.com
keep_email_private: true
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
is_admin: false
avatar: avatar28
avatar_email: user28@example.com
num_repos: 0
num_stars: 0
num_followers: 0
num_following: 0
is_active: true

2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v109.go
Original file line number Diff line number Diff line change
@@ -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))
}
32 changes: 32 additions & 0 deletions models/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions models/org_team.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions models/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,22 @@ 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)
}
}

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

Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 32 additions & 20 deletions models/repo_collaboration.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,37 @@ 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 {
return nil
}
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()
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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()
}

Expand Down
4 changes: 2 additions & 2 deletions models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
Loading

0 comments on commit 69a255d

Please sign in to comment.