Skip to content

Commit 69a255d

Browse files
davidsvantessonlunny
authored andcommitted
Team permission to create repository in organization (#8312)
* Add team permission setting to allow creating repo in organization. Signed-off-by: David Svantesson <[email protected]> * Add test case for creating repo when have team creation access. Signed-off-by: David Svantesson <[email protected]> * build error: should omit comparison to bool constant Signed-off-by: David Svantesson <[email protected]> * 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
1 parent 35c3ea9 commit 69a255d

File tree

27 files changed

+252
-63
lines changed

27 files changed

+252
-63
lines changed

integrations/api_repo_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ func TestAPIOrgRepoCreate(t *testing.T) {
347347
{ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated},
348348
{ctxUserID: 2, orgName: "user3", repoName: "repo-own", expectedStatus: http.StatusCreated},
349349
{ctxUserID: 2, orgName: "user6", repoName: "repo-bad-org", expectedStatus: http.StatusForbidden},
350+
{ctxUserID: 28, orgName: "user3", repoName: "repo-creator", expectedStatus: http.StatusCreated},
351+
{ctxUserID: 28, orgName: "user6", repoName: "repo-not-creator", expectedStatus: http.StatusForbidden},
350352
}
351353

352354
prepareTestEnv(t)

models/fixtures/org_user.yml

+13
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,16 @@
4545
uid: 24
4646
org_id: 25
4747
is_public: true
48+
49+
-
50+
id: 9
51+
uid: 28
52+
org_id: 3
53+
is_public: true
54+
55+
-
56+
id: 10
57+
uid: 28
58+
org_id: 6
59+
is_public: true
60+

models/fixtures/team.yml

+20
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,23 @@
9696
authorize: 1 # read
9797
num_repos: 0
9898
num_members: 0
99+
100+
-
101+
id: 12
102+
org_id: 3
103+
lower_name: team12creators
104+
name: team12Creators
105+
authorize: 3 # admin
106+
num_repos: 0
107+
num_members: 1
108+
can_create_org_repo: true
109+
110+
-
111+
id: 13
112+
org_id: 6
113+
lower_name: team13notcreators
114+
name: team13NotCreators
115+
authorize: 3 # admin
116+
num_repos: 0
117+
num_members: 1
118+
can_create_org_repo: false

models/fixtures/team_user.yml

+12
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,15 @@
6969
org_id: 25
7070
team_id: 10
7171
uid: 24
72+
73+
-
74+
id: 13
75+
org_id: 3
76+
team_id: 12
77+
uid: 28
78+
79+
-
80+
id: 14
81+
org_id: 6
82+
team_id: 13
83+
uid: 28

models/fixtures/user.yml

+24-4
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
avatar: avatar3
5151
avatar_email: [email protected]
5252
num_repos: 3
53-
num_members: 2
54-
num_teams: 3
53+
num_members: 3
54+
num_teams: 4
5555

5656
-
5757
id: 4
@@ -102,8 +102,8 @@
102102
avatar: avatar6
103103
avatar_email: [email protected]
104104
num_repos: 0
105-
num_members: 1
106-
num_teams: 1
105+
num_members: 2
106+
num_teams: 2
107107

108108
-
109109
id: 7
@@ -443,3 +443,23 @@
443443
avatar: avatar27
444444
avatar_email: [email protected]
445445
num_repos: 2
446+
447+
-
448+
id: 28
449+
lower_name: user28
450+
name: user28
451+
full_name: "user27"
452+
453+
keep_email_private: true
454+
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
455+
type: 0 # individual
456+
salt: ZogKvWdyEx
457+
is_admin: false
458+
avatar: avatar28
459+
avatar_email: [email protected]
460+
num_repos: 0
461+
num_stars: 0
462+
num_followers: 0
463+
num_following: 0
464+
is_active: true
465+

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ var migrations = []Migration{
272272
NewMigration("Add template options to repository", addTemplateToRepo),
273273
// v108 -> v109
274274
NewMigration("Add comment_id on table notification", addCommentIDOnNotification),
275+
// v109 -> v110
276+
NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
275277
}
276278

277279
// Migrate database to current version

models/migrations/v109.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addCanCreateOrgRepoColumnForTeam(x *xorm.Engine) error {
12+
type Team struct {
13+
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
14+
}
15+
16+
return x.Sync2(new(Team))
17+
}

models/org.go

+32
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ func (org *User) IsOrgMember(uid int64) (bool, error) {
2929
return IsOrganizationMember(org.ID, uid)
3030
}
3131

32+
// CanCreateOrgRepo returns true if given user can create repo in organization
33+
func (org *User) CanCreateOrgRepo(uid int64) (bool, error) {
34+
return CanCreateOrgRepo(org.ID, uid)
35+
}
36+
3237
func (org *User) getTeam(e Engine, name string) (*Team, error) {
3338
return getTeam(e, org.ID, name)
3439
}
@@ -158,6 +163,7 @@ func CreateOrganization(org, owner *User) (err error) {
158163
Authorize: AccessModeOwner,
159164
NumMembers: 1,
160165
IncludesAllRepositories: true,
166+
CanCreateOrgRepo: true,
161167
}
162168
if _, err = sess.Insert(t); err != nil {
163169
return fmt.Errorf("insert owner team: %v", err)
@@ -339,6 +345,19 @@ func IsPublicMembership(orgID, uid int64) (bool, error) {
339345
Exist()
340346
}
341347

348+
// CanCreateOrgRepo returns true if user can create repo in organization
349+
func CanCreateOrgRepo(orgID, uid int64) (bool, error) {
350+
if owner, err := IsOrganizationOwner(orgID, uid); owner || err != nil {
351+
return owner, err
352+
}
353+
return x.
354+
Where(builder.Eq{"team.can_create_org_repo": true}).
355+
Join("INNER", "team_user", "team_user.team_id = team.id").
356+
And("team_user.uid = ?", uid).
357+
And("team_user.org_id = ?", orgID).
358+
Exist(new(Team))
359+
}
360+
342361
func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) {
343362
orgs := make([]*User, 0, 10)
344363
if !showAll {
@@ -418,6 +437,19 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
418437
return getOwnedOrgsByUserID(x.Desc(desc), userID)
419438
}
420439

440+
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
441+
// are allowed to create repos.
442+
func GetOrgsCanCreateRepoByUserID(userID int64) ([]*User, error) {
443+
orgs := make([]*User, 0, 10)
444+
445+
return orgs, x.Join("INNER", "`team_user`", "`team_user`.org_id=`user`.id").
446+
Join("INNER", "`team`", "`team`.id=`team_user`.team_id").
447+
Where("`team_user`.uid=?", userID).
448+
And(builder.Eq{"`team`.authorize": AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})).
449+
Desc("`user`.updated_unix").
450+
Find(&orgs)
451+
}
452+
421453
// GetOrgUsersByUserID returns all organization-user relations by user ID.
422454
func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) {
423455
ous := make([]*OrgUser, 0, 10)

models/org_team.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Team struct {
3434
NumMembers int
3535
Units []*TeamUnit `xorm:"-"`
3636
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
37+
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
3738
}
3839

3940
// SearchTeamOptions holds the search options

models/org_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,22 @@ func TestUser_GetTeams(t *testing.T) {
8787
assert.NoError(t, PrepareTestDatabase())
8888
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
8989
assert.NoError(t, org.GetTeams())
90-
if assert.Len(t, org.Teams, 3) {
90+
if assert.Len(t, org.Teams, 4) {
9191
assert.Equal(t, int64(1), org.Teams[0].ID)
9292
assert.Equal(t, int64(2), org.Teams[1].ID)
93-
assert.Equal(t, int64(7), org.Teams[2].ID)
93+
assert.Equal(t, int64(12), org.Teams[2].ID)
94+
assert.Equal(t, int64(7), org.Teams[3].ID)
9495
}
9596
}
9697

9798
func TestUser_GetMembers(t *testing.T) {
9899
assert.NoError(t, PrepareTestDatabase())
99100
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
100101
assert.NoError(t, org.GetMembers())
101-
if assert.Len(t, org.Members, 2) {
102+
if assert.Len(t, org.Members, 3) {
102103
assert.Equal(t, int64(2), org.Members[0].ID)
103-
assert.Equal(t, int64(4), org.Members[1].ID)
104+
assert.Equal(t, int64(28), org.Members[1].ID)
105+
assert.Equal(t, int64(4), org.Members[2].ID)
104106
}
105107
}
106108

@@ -395,7 +397,7 @@ func TestGetOrgUsersByOrgID(t *testing.T) {
395397

396398
orgUsers, err := GetOrgUsersByOrgID(3)
397399
assert.NoError(t, err)
398-
if assert.Len(t, orgUsers, 2) {
400+
if assert.Len(t, orgUsers, 3) {
399401
assert.Equal(t, OrgUser{
400402
ID: orgUsers[0].ID,
401403
OrgID: 3,

models/repo.go

+12
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,18 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
15861586
}
15871587
}
15881588
}
1589+
1590+
if isAdmin, err := isUserRepoAdmin(e, repo, doer); err != nil {
1591+
return fmt.Errorf("isUserRepoAdmin: %v", err)
1592+
} else if !isAdmin {
1593+
// Make creator repo admin if it wan't assigned automatically
1594+
if err = repo.addCollaborator(e, doer); err != nil {
1595+
return fmt.Errorf("AddCollaborator: %v", err)
1596+
}
1597+
if err = repo.changeCollaborationAccessMode(e, doer.ID, AccessModeAdmin); err != nil {
1598+
return fmt.Errorf("ChangeCollaborationAccessMode: %v", err)
1599+
}
1600+
}
15891601
} else if err = repo.recalculateAccesses(e); err != nil {
15901602
// Organization automatically called this in addRepository method.
15911603
return fmt.Errorf("recalculateAccesses: %v", err)

models/repo_collaboration.go

+32-20
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,37 @@ type Collaboration struct {
1616
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
1717
}
1818

19-
// AddCollaborator adds new collaboration to a repository with default access mode.
20-
func (repo *Repository) AddCollaborator(u *User) error {
19+
func (repo *Repository) addCollaborator(e Engine, u *User) error {
2120
collaboration := &Collaboration{
2221
RepoID: repo.ID,
2322
UserID: u.ID,
2423
}
2524

26-
has, err := x.Get(collaboration)
25+
has, err := e.Get(collaboration)
2726
if err != nil {
2827
return err
2928
} else if has {
3029
return nil
3130
}
3231
collaboration.Mode = AccessModeWrite
3332

34-
sess := x.NewSession()
35-
defer sess.Close()
36-
if err = sess.Begin(); err != nil {
33+
if _, err = e.InsertOne(collaboration); err != nil {
3734
return err
3835
}
3936

40-
if _, err = sess.InsertOne(collaboration); err != nil {
37+
return repo.recalculateUserAccess(e, u.ID)
38+
}
39+
40+
// AddCollaborator adds new collaboration to a repository with default access mode.
41+
func (repo *Repository) AddCollaborator(u *User) error {
42+
sess := x.NewSession()
43+
defer sess.Close()
44+
if err := sess.Begin(); err != nil {
4145
return err
4246
}
4347

44-
if err = repo.recalculateUserAccess(sess, u.ID); err != nil {
45-
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err)
48+
if err := repo.addCollaborator(sess, u); err != nil {
49+
return err
4650
}
4751

4852
return sess.Commit()
@@ -105,8 +109,7 @@ func (repo *Repository) IsCollaborator(userID int64) (bool, error) {
105109
return repo.isCollaborator(x, userID)
106110
}
107111

108-
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
109-
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error {
112+
func (repo *Repository) changeCollaborationAccessMode(e Engine, uid int64, mode AccessMode) error {
110113
// Discard invalid input
111114
if mode <= AccessModeNone || mode > AccessModeOwner {
112115
return nil
@@ -116,7 +119,7 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode
116119
RepoID: repo.ID,
117120
UserID: uid,
118121
}
119-
has, err := x.Get(collaboration)
122+
has, err := e.Get(collaboration)
120123
if err != nil {
121124
return fmt.Errorf("get collaboration: %v", err)
122125
} else if !has {
@@ -128,21 +131,30 @@ func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode
128131
}
129132
collaboration.Mode = mode
130133

131-
sess := x.NewSession()
132-
defer sess.Close()
133-
if err = sess.Begin(); err != nil {
134-
return err
135-
}
136-
137-
if _, err = sess.
134+
if _, err = e.
138135
ID(collaboration.ID).
139136
Cols("mode").
140137
Update(collaboration); err != nil {
141138
return fmt.Errorf("update collaboration: %v", err)
142-
} else if _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
139+
} else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
143140
return fmt.Errorf("update access table: %v", err)
144141
}
145142

143+
return nil
144+
}
145+
146+
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
147+
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error {
148+
sess := x.NewSession()
149+
defer sess.Close()
150+
if err := sess.Begin(); err != nil {
151+
return err
152+
}
153+
154+
if err := repo.changeCollaborationAccessMode(sess, uid, mode); err != nil {
155+
return err
156+
}
157+
146158
return sess.Commit()
147159
}
148160

models/user_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ func TestSearchUsers(t *testing.T) {
153153
}
154154

155155
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1},
156-
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27})
156+
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28})
157157

158158
testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
159159
[]int64{9})
160160

161161
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
162-
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24})
162+
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28})
163163

164164
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
165165
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})

0 commit comments

Comments
 (0)