* 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
{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)
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
+
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
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
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
+
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
--- /dev/null
+// 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))
+}
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)
}
Authorize: AccessModeOwner,
NumMembers: 1,
IncludesAllRepositories: true,
+ CanCreateOrgRepo: true,
}
if _, err = sess.Insert(t); err != nil {
return fmt.Errorf("insert owner team: %v", err)
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 {
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)
NumMembers int
Units []*TeamUnit `xorm:"-"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
+ CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
}
// SearchTeamOptions holds the search options
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)
}
}
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)
}
}
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,
}
}
}
+
+ 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)
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 {
}
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()
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
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 {
}
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()
}
}
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})
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{}},
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
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{}},
// 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
// 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
}
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 {
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.
}
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
Name: team.Name,
Description: team.Description,
IncludesAllRepositories: team.IncludesAllRepositories,
+ CanCreateOrgRepo: team.CanCreateOrgRepo,
Permission: team.Authorize.String(),
Units: team.GetUnitNames(),
}
// 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
// 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
// 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"`
}
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
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
Name: form.Name,
Description: form.Description,
IncludesAllRepositories: form.IncludesAllRepositories,
+ CanCreateOrgRepo: form.CanCreateOrgRepo,
Authorize: models.ParseAccessMode(form.Permission),
}
team := ctx.Org.Team
team.Description = form.Description
unitTypes := models.FindUnitTypes(form.Units...)
+ team.CanCreateOrgRepo = form.CanCreateOrgRepo
isAuthChanged := false
isIncludeAllChanged := false
}
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
}
}
Description: form.Description,
Authorize: models.ParseAccessMode(form.Permission),
IncludesAllRepositories: includesAllRepositories,
+ CanCreateOrgRepo: form.CanCreateOrgRepo,
}
if t.Authorize < models.AccessModeOwner {
return
}
}
+ t.CanCreateOrgRepo = form.CanCreateOrgRepo
if ctx.HasError() {
ctx.HTML(200, tplTeamNew)
}
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
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
}
<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>
<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>
{{.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}}
"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"