Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Team permission allow different unit has different permission #17811

Merged
merged 62 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
587ac0f
Team permission allow different unit has different permission
lunny Nov 25, 2021
eb8237f
Finish the interface and the logic
lunny Nov 25, 2021
35ae85a
Fix lint
lunny Nov 25, 2021
b4ea024
Fix translation
lunny Nov 25, 2021
fbcb9d5
align center for table cell content
lunny Nov 25, 2021
deb1901
Fix fixture
lunny Nov 25, 2021
1819e25
merge
lunny Nov 25, 2021
89197aa
Fix test
lunny Nov 27, 2021
f0ed083
Add deprecated
lunny Nov 27, 2021
5e4f751
Improve code
lunny Nov 27, 2021
c53ce3d
Add tooltip
lunny Nov 28, 2021
41b516d
Fix swagger
lunny Nov 28, 2021
fbe50f6
Fix newline
lunny Nov 28, 2021
f81b33c
Fix tests
lunny Nov 28, 2021
0135739
Fix tests
lunny Nov 30, 2021
9d92b30
Fix test
lunny Dec 1, 2021
7c77e06
Fix test
lunny Dec 1, 2021
1396e74
Max permission of external wiki and issues should be read
lunny Dec 2, 2021
0738b2c
Move team units with limited max level below units table
lafriks Dec 3, 2021
dc09224
Some improvements
lunny Dec 6, 2021
0bfffa2
Fix lint
lunny Dec 6, 2021
fe09394
Some improvements
lunny Dec 8, 2021
fd15893
Fix template variables
lunny Dec 13, 2021
1bce43a
Add permission docs
lunny Dec 13, 2021
ea2f677
improve doc
lunny Dec 16, 2021
367d1d0
Fix fixture
lunny Dec 16, 2021
232affa
Fix bug
lunny Jan 2, 2022
c4ae43c
Fix some bug
lunny Jan 4, 2022
eda5846
Merge branch 'master' into lunny/team_multiple_units
6543 Jan 4, 2022
dbfa40f
fix
6543 Jan 4, 2022
23ee2b5
gofumpt
6543 Jan 4, 2022
12ad6dd
Integration test for migration (#18124)
realaravinth Jan 4, 2022
8bc9da5
Team permission allow different unit has different permission
lunny Nov 25, 2021
ecc65cf
Finish the interface and the logic
lunny Nov 25, 2021
7f03e0c
Fix lint
lunny Nov 25, 2021
43808fc
Fix translation
lunny Nov 25, 2021
1d1419c
align center for table cell content
lunny Nov 25, 2021
d42a89d
Fix fixture
lunny Nov 25, 2021
58f8f0e
merge
lunny Nov 25, 2021
24bccf4
Fix test
lunny Nov 27, 2021
3f847a3
Add deprecated
lunny Nov 27, 2021
910f967
Improve code
lunny Nov 27, 2021
1079027
Add tooltip
lunny Nov 28, 2021
827826e
Fix swagger
lunny Nov 28, 2021
12ea655
Fix newline
lunny Nov 28, 2021
4209d43
Fix tests
lunny Nov 28, 2021
aed9f65
Fix tests
lunny Nov 30, 2021
9884caf
Fix test
lunny Dec 1, 2021
85d952d
Fix test
lunny Dec 1, 2021
034630f
Max permission of external wiki and issues should be read
lunny Dec 2, 2021
e21c449
Move team units with limited max level below units table
lafriks Dec 3, 2021
4455889
Some improvements
lunny Dec 6, 2021
35eafb1
Fix lint
lunny Dec 6, 2021
45962e4
Some improvements
lunny Dec 8, 2021
9c4f1e9
Fix template variables
lunny Dec 13, 2021
90bb10b
Add permission docs
lunny Dec 13, 2021
83a03cc
improve doc
lunny Dec 16, 2021
252d1a8
Fix fixture
lunny Dec 16, 2021
d20b90e
Fix bug
lunny Jan 2, 2022
fb1cc7e
Fix some bug
lunny Jan 4, 2022
55164ac
Fix bug
lunny Jan 5, 2022
2cd4b3d
Merge fmt
lunny Jan 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/content/doc/usage/permissions.en-us.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
date: "2021-12-13:10:10+08:00"
title: "Permissions"
slug: "permissions"
weight: 14
toc: false
draft: false
menu:
sidebar:
parent: "usage"
name: "Permissions"
weight: 14
identifier: "permissions"
---

# Permissions

**Table of Contents**

{{< toc >}}

Gitea supports permissions for repository so that you can give different access for different people. At first, we need to know about `Unit`.

## Unit

In Gitea, we call a sub module of a repository `Unit`. Now we have following units.

| Name | Description | Permissions |
| --------------- | ---------------------------------------------------- | ----------- |
| Code | Access source code, files, commits and branches. | Read Write |
| Issues | Organize bug reports, tasks and milestones. | Read Write |
| PullRequests | Enable pull requests and code reviews. | Read Write |
| Releases | Track project versions and downloads. | Read Write |
| Wiki | Write and share documentation with collaborators. | Read Write |
| ExternalWiki | Link to an external wiki | Read |
| ExternalTracker | Link to an external issue tracker | Read |
| Projects | The URL to the template repository | Read Write |
| Settings | Manage the repository | Admin |

With different permissions, people could do different things with these units.

| Name | Read | Write | Admin |
| --------------- | ------------------------------------------------- | ---------------------------- | ------------------------- |
| Code | View code trees, files, commits, branches and etc. | Push codes. | - |
| Issues | View issues and create new issues. | Add labels, assign, close | - |
| PullRequests | View pull requests and create new pull requests. | Add labels, assign, close | - |
| Releases | View releases and download files. | Create/Edit releases | - |
| Wiki | View wiki pages. Clone the wiki repository. | Create/Edit wiki pages, push | - |
| ExternalWiki | Link to an external wiki | - | - |
| ExternalTracker | Link to an external issue tracker | - | - |
| Projects | View the boards | Change issues across boards | - |
| Settings | - | - | Manage the repository |

And there are some differences for permissions between individual repositories and organization repositories.

## Individual Repository

For individual repositories, the creators are the only owners of repositories and have no limit to change anything of this
repository or delete it. Repositories owners could add collaborators to help maintain the repositories. Collaborators could have `Read`, `Write` and `Admin` permissions.

## Organization Repository

Different from individual repositories, the owner of organization repositories are the owner team of this organization.

### Team

A team in an organization has unit permissions settings. It can have members and repositories scope. A team could access all the repositories in this organization or special repositories changed by the owner team. A team could also be allowed to create new
repositories.

The owner team will be created when the organization created and the creator will become the first member of the owner team.
Notice Gitea will not allow a people is a member of organization but not in any team. The owner team could not be deleted and only
members of owner team could create a new team. Admin team could be created to manage some of repositories, members of admin team
could do anything with these repositories. Generate team could be created by the owner team to do the permissions allowed operations.
4 changes: 3 additions & 1 deletion integrations/api_repo_teams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"testing"

repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"

"github.com/stretchr/testify/assert"
)
Expand All @@ -36,7 +38,7 @@ func TestAPIRepoTeams(t *testing.T) {
if assert.Len(t, teams, 2) {
assert.EqualValues(t, "Owners", teams[0].Name)
assert.False(t, teams[0].CanCreateOrgRepo)
assert.EqualValues(t, []string{"repo.code", "repo.issues", "repo.pulls", "repo.releases", "repo.wiki", "repo.ext_wiki", "repo.ext_issues"}, teams[0].Units)
assert.True(t, util.IsEqualSlice(unit.AllUnitKeyNames(), teams[0].Units), fmt.Sprintf("%v == %v", unit.AllUnitKeyNames(), teams[0].Units))
assert.EqualValues(t, "owner", teams[0].Permission)

assert.EqualValues(t, "test_team", teams[1].Name)
Expand Down
114 changes: 96 additions & 18 deletions integrations/api_team_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/convert"
Expand Down Expand Up @@ -65,11 +66,12 @@ func TestAPITeam(t *testing.T) {
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
resp = session.MakeRequest(t, req, http.StatusCreated)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
teamToCreate.Permission, teamToCreate.Units)
teamToCreate.Permission, teamToCreate.Units, nil)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
teamToCreate.Permission, teamToCreate.Units)
teamToCreate.Permission, teamToCreate.Units, nil)
teamID := apiTeam.ID

// Edit team.
Expand All @@ -85,51 +87,128 @@ func TestAPITeam(t *testing.T) {

req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, teamToEdit.Units)
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, teamToEdit.Units)
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)

// Edit team Description only
editDescription = "first team"
teamToEditDesc := api.EditTeamOption{Description: &editDescription}
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, teamToEdit.Units)
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, teamToEdit.Units)
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)

// Read team.
teamRead := unittest.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
assert.NoError(t, teamRead.GetUnits())
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
teamRead.Authorize.String(), teamRead.GetUnitNames())
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())

// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
session.MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &models.Team{ID: teamID})

// create team again via UnitsMap
// Create team.
teamToCreate = &api.CreateTeamOption{
Name: "team2",
Description: "team two",
IncludesAllRepositories: true,
Permission: "write",
UnitsMap: map[string]string{"repo.code": "read", "repo.issues": "write", "repo.wiki": "none"},
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
resp = session.MakeRequest(t, req, http.StatusCreated)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"read", nil, teamToCreate.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"read", nil, teamToCreate.UnitsMap)
teamID = apiTeam.ID

// Edit team.
editDescription = "team 1"
editFalse = false
teamToEdit = &api.EditTeamOption{
Name: "teamtwo",
Description: &editDescription,
Permission: "write",
IncludesAllRepositories: &editFalse,
UnitsMap: map[string]string{"repo.code": "read", "repo.pulls": "read", "repo.releases": "write"},
}

req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
"read", nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
"read", nil, teamToEdit.UnitsMap)

// Edit team Description only
editDescription = "second team"
teamToEditDesc = api.EditTeamOption{Description: &editDescription}
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
"read", nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
"read", nil, teamToEdit.UnitsMap)

// Read team.
teamRead = unittest.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
resp = session.MakeRequest(t, req, http.StatusOK)
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
assert.NoError(t, teamRead.GetUnits())
checkTeamResponse(t, &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())

// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
session.MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &models.Team{ID: teamID})
}

func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string) {
assert.Equal(t, name, apiTeam.Name, "name")
assert.Equal(t, description, apiTeam.Description, "description")
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
assert.Equal(t, permission, apiTeam.Permission, "permission")
sort.StringSlice(units).Sort()
sort.StringSlice(apiTeam.Units).Sort()
assert.EqualValues(t, units, apiTeam.Units, "units")
func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
t.Run(name+description, func(t *testing.T) {
assert.Equal(t, name, apiTeam.Name, "name")
assert.Equal(t, description, apiTeam.Description, "description")
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
assert.Equal(t, permission, apiTeam.Permission, "permission")
if units != nil {
sort.StringSlice(units).Sort()
sort.StringSlice(apiTeam.Units).Sort()
assert.EqualValues(t, units, apiTeam.Units, "units")
}
if unitsMap != nil {
assert.EqualValues(t, unitsMap, apiTeam.UnitsMap, "unitsMap")
}
})
}

func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string) {
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
team := unittest.AssertExistsAndLoadBean(t, &models.Team{ID: id}).(*models.Team)
assert.NoError(t, team.GetUnits(), "GetUnits")
checkTeamResponse(t, convert.ToTeam(team), name, description, includesAllRepositories, permission, units)
checkTeamResponse(t, convert.ToTeam(team), name, description, includesAllRepositories, permission, units, unitsMap)
}

type TeamSearchResults struct {
Expand Down Expand Up @@ -162,5 +241,4 @@ func TestAPITeamSearch(t *testing.T) {
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "team")
req.Header.Add("X-Csrf-Token", csrf)
session.MakeRequest(t, req, http.StatusForbidden)

}
104 changes: 104 additions & 0 deletions integrations/dump_restore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2022 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 integrations

import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
"testing"

repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/migrations"

"github.com/stretchr/testify/assert"
)

func TestDumpRestore(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
setting.Migrations.AllowLocalNetworks = true
AppVer := setting.AppVer
// Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
setting.AppVer = "1.16.0"
defer func() {
setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
setting.AppVer = AppVer
}()

assert.NoError(t, migrations.Init())

reponame := "repo1"

basePath, err := os.MkdirTemp("", reponame)
assert.NoError(t, err)
defer util.RemoveAll(basePath)

repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository)
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)

//
// Phase 1: dump repo1 from the Gitea instance to the filesystem
//

ctx := context.Background()
var opts = migrations.MigrateOptions{
GitServiceType: structs.GiteaService,
Issues: true,
Comments: true,
AuthToken: token,
CloneAddr: repo.CloneLink().HTTPS,
RepoName: reponame,
}
err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
assert.NoError(t, err)

//
// Verify desired side effects of the dump
//
d := filepath.Join(basePath, repo.OwnerName, repo.Name)
for _, f := range []string{"repo.yml", "topic.yml", "issue.yml"} {
assert.FileExists(t, filepath.Join(d, f))
}

//
// Phase 2: restore from the filesystem to the Gitea instance in restoredrepo
//

newreponame := "restoredrepo"
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"issues", "comments"})
assert.NoError(t, err)

newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame}).(*repo_model.Repository)

//
// Phase 3: dump restoredrepo from the Gitea instance to the filesystem
//
opts.RepoName = newreponame
opts.CloneAddr = newrepo.CloneLink().HTTPS
err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
assert.NoError(t, err)

//
// Verify the dump of restoredrepo is the same as the dump of repo1
//
newd := filepath.Join(basePath, newrepo.OwnerName, newrepo.Name)
beforeBytes, err := os.ReadFile(filepath.Join(d, "repo.yml"))
assert.NoError(t, err)
before := strings.ReplaceAll(string(beforeBytes), reponame, newreponame)
after, err := os.ReadFile(filepath.Join(newd, "repo.yml"))
assert.NoError(t, err)
assert.EqualValues(t, before, string(after))
})
}
7 changes: 3 additions & 4 deletions integrations/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ func TestOrgRestrictedUser(t *testing.T) {
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
teamToCreate.Permission, teamToCreate.Units)
teamToCreate.Permission, teamToCreate.Units, nil)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
teamToCreate.Permission, teamToCreate.Units)
//teamID := apiTeam.ID
teamToCreate.Permission, teamToCreate.Units, nil)
// teamID := apiTeam.ID

// Now we need to add the restricted user to the team
req = NewRequest(t, "PUT",
Expand All @@ -172,5 +172,4 @@ func TestOrgRestrictedUser(t *testing.T) {

req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName))
restrictedSession.MakeRequest(t, req, http.StatusOK)

}
Loading