Skip to content

Commit 8760af7

Browse files
lunnylafriks6543realaravinth
authored
Team permission allow different unit has different permission (#17811)
* Team permission allow different unit has different permission * Finish the interface and the logic * Fix lint * Fix translation * align center for table cell content * Fix fixture * merge * Fix test * Add deprecated * Improve code * Add tooltip * Fix swagger * Fix newline * Fix tests * Fix tests * Fix test * Fix test * Max permission of external wiki and issues should be read * Move team units with limited max level below units table * Update label and column names * Some improvements * Fix lint * Some improvements * Fix template variables * Add permission docs * improve doc * Fix fixture * Fix bug * Fix some bug * fix * gofumpt * Integration test for migration (#18124) integrations: basic test for Gitea {dump,restore}-repo This is a first step for integration testing of DumpRepository and RestoreRepository. It: runs a Gitea server, dumps a repo via DumpRepository to the filesystem, restores the repo via RestoreRepository from the filesystem, dumps the restored repository to the filesystem, compares the first and second dump and expects them to be identical The verification is trivial and the goal is to add more tests for each topic of the dump. Signed-off-by: Loïc Dachary <loic@dachary.org> * Team permission allow different unit has different permission * Finish the interface and the logic * Fix lint * Fix translation * align center for table cell content * Fix fixture * merge * Fix test * Add deprecated * Improve code * Add tooltip * Fix swagger * Fix newline * Fix tests * Fix tests * Fix test * Fix test * Max permission of external wiki and issues should be read * Move team units with limited max level below units table * Update label and column names * Some improvements * Fix lint * Some improvements * Fix template variables * Add permission docs * improve doc * Fix fixture * Fix bug * Fix some bug * Fix bug Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Aravinth Manivannan <realaravinth@batsense.net>
1 parent 12ad6dd commit 8760af7

File tree

27 files changed

+610
-170
lines changed

27 files changed

+610
-170
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
date: "2021-12-13:10:10+08:00"
3+
title: "Permissions"
4+
slug: "permissions"
5+
weight: 14
6+
toc: false
7+
draft: false
8+
menu:
9+
sidebar:
10+
parent: "usage"
11+
name: "Permissions"
12+
weight: 14
13+
identifier: "permissions"
14+
---
15+
16+
# Permissions
17+
18+
**Table of Contents**
19+
20+
{{< toc >}}
21+
22+
Gitea supports permissions for repository so that you can give different access for different people. At first, we need to know about `Unit`.
23+
24+
## Unit
25+
26+
In Gitea, we call a sub module of a repository `Unit`. Now we have following units.
27+
28+
| Name | Description | Permissions |
29+
| --------------- | ---------------------------------------------------- | ----------- |
30+
| Code | Access source code, files, commits and branches. | Read Write |
31+
| Issues | Organize bug reports, tasks and milestones. | Read Write |
32+
| PullRequests | Enable pull requests and code reviews. | Read Write |
33+
| Releases | Track project versions and downloads. | Read Write |
34+
| Wiki | Write and share documentation with collaborators. | Read Write |
35+
| ExternalWiki | Link to an external wiki | Read |
36+
| ExternalTracker | Link to an external issue tracker | Read |
37+
| Projects | The URL to the template repository | Read Write |
38+
| Settings | Manage the repository | Admin |
39+
40+
With different permissions, people could do different things with these units.
41+
42+
| Name | Read | Write | Admin |
43+
| --------------- | ------------------------------------------------- | ---------------------------- | ------------------------- |
44+
| Code | View code trees, files, commits, branches and etc. | Push codes. | - |
45+
| Issues | View issues and create new issues. | Add labels, assign, close | - |
46+
| PullRequests | View pull requests and create new pull requests. | Add labels, assign, close | - |
47+
| Releases | View releases and download files. | Create/Edit releases | - |
48+
| Wiki | View wiki pages. Clone the wiki repository. | Create/Edit wiki pages, push | - |
49+
| ExternalWiki | Link to an external wiki | - | - |
50+
| ExternalTracker | Link to an external issue tracker | - | - |
51+
| Projects | View the boards | Change issues across boards | - |
52+
| Settings | - | - | Manage the repository |
53+
54+
And there are some differences for permissions between individual repositories and organization repositories.
55+
56+
## Individual Repository
57+
58+
For individual repositories, the creators are the only owners of repositories and have no limit to change anything of this
59+
repository or delete it. Repositories owners could add collaborators to help maintain the repositories. Collaborators could have `Read`, `Write` and `Admin` permissions.
60+
61+
## Organization Repository
62+
63+
Different from individual repositories, the owner of organization repositories are the owner team of this organization.
64+
65+
### Team
66+
67+
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
68+
repositories.
69+
70+
The owner team will be created when the organization created and the creator will become the first member of the owner team.
71+
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
72+
members of owner team could create a new team. Admin team could be created to manage some of repositories, members of admin team
73+
could do anything with these repositories. Generate team could be created by the owner team to do the permissions allowed operations.

integrations/api_repo_teams_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"testing"
1111

1212
repo_model "code.gitea.io/gitea/models/repo"
13+
"code.gitea.io/gitea/models/unit"
1314
"code.gitea.io/gitea/models/unittest"
1415
user_model "code.gitea.io/gitea/models/user"
1516
api "code.gitea.io/gitea/modules/structs"
17+
"code.gitea.io/gitea/modules/util"
1618

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

4244
assert.EqualValues(t, "test_team", teams[1].Name)

integrations/api_team_test.go

+96-18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212

1313
"code.gitea.io/gitea/models"
14+
"code.gitea.io/gitea/models/unit"
1415
"code.gitea.io/gitea/models/unittest"
1516
user_model "code.gitea.io/gitea/models/user"
1617
"code.gitea.io/gitea/modules/convert"
@@ -65,11 +66,12 @@ func TestAPITeam(t *testing.T) {
6566
}
6667
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
6768
resp = session.MakeRequest(t, req, http.StatusCreated)
69+
apiTeam = api.Team{}
6870
DecodeJSON(t, resp, &apiTeam)
6971
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
70-
teamToCreate.Permission, teamToCreate.Units)
72+
teamToCreate.Permission, teamToCreate.Units, nil)
7173
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
72-
teamToCreate.Permission, teamToCreate.Units)
74+
teamToCreate.Permission, teamToCreate.Units, nil)
7375
teamID := apiTeam.ID
7476

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

8688
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
8789
resp = session.MakeRequest(t, req, http.StatusOK)
90+
apiTeam = api.Team{}
8891
DecodeJSON(t, resp, &apiTeam)
8992
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
90-
teamToEdit.Permission, teamToEdit.Units)
93+
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
9194
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
92-
teamToEdit.Permission, teamToEdit.Units)
95+
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
9396

9497
// Edit team Description only
9598
editDescription = "first team"
9699
teamToEditDesc := api.EditTeamOption{Description: &editDescription}
97100
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
98101
resp = session.MakeRequest(t, req, http.StatusOK)
102+
apiTeam = api.Team{}
99103
DecodeJSON(t, resp, &apiTeam)
100104
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
101-
teamToEdit.Permission, teamToEdit.Units)
105+
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
102106
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
103-
teamToEdit.Permission, teamToEdit.Units)
107+
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
104108

105109
// Read team.
106110
teamRead := unittest.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
111+
assert.NoError(t, teamRead.GetUnits())
107112
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
108113
resp = session.MakeRequest(t, req, http.StatusOK)
114+
apiTeam = api.Team{}
109115
DecodeJSON(t, resp, &apiTeam)
110116
checkTeamResponse(t, &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
111-
teamRead.Authorize.String(), teamRead.GetUnitNames())
117+
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
118+
119+
// Delete team.
120+
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
121+
session.MakeRequest(t, req, http.StatusNoContent)
122+
unittest.AssertNotExistsBean(t, &models.Team{ID: teamID})
123+
124+
// create team again via UnitsMap
125+
// Create team.
126+
teamToCreate = &api.CreateTeamOption{
127+
Name: "team2",
128+
Description: "team two",
129+
IncludesAllRepositories: true,
130+
Permission: "write",
131+
UnitsMap: map[string]string{"repo.code": "read", "repo.issues": "write", "repo.wiki": "none"},
132+
}
133+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
134+
resp = session.MakeRequest(t, req, http.StatusCreated)
135+
apiTeam = api.Team{}
136+
DecodeJSON(t, resp, &apiTeam)
137+
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
138+
"read", nil, teamToCreate.UnitsMap)
139+
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
140+
"read", nil, teamToCreate.UnitsMap)
141+
teamID = apiTeam.ID
142+
143+
// Edit team.
144+
editDescription = "team 1"
145+
editFalse = false
146+
teamToEdit = &api.EditTeamOption{
147+
Name: "teamtwo",
148+
Description: &editDescription,
149+
Permission: "write",
150+
IncludesAllRepositories: &editFalse,
151+
UnitsMap: map[string]string{"repo.code": "read", "repo.pulls": "read", "repo.releases": "write"},
152+
}
153+
154+
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
155+
resp = session.MakeRequest(t, req, http.StatusOK)
156+
apiTeam = api.Team{}
157+
DecodeJSON(t, resp, &apiTeam)
158+
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
159+
"read", nil, teamToEdit.UnitsMap)
160+
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
161+
"read", nil, teamToEdit.UnitsMap)
162+
163+
// Edit team Description only
164+
editDescription = "second team"
165+
teamToEditDesc = api.EditTeamOption{Description: &editDescription}
166+
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
167+
resp = session.MakeRequest(t, req, http.StatusOK)
168+
apiTeam = api.Team{}
169+
DecodeJSON(t, resp, &apiTeam)
170+
checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
171+
"read", nil, teamToEdit.UnitsMap)
172+
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
173+
"read", nil, teamToEdit.UnitsMap)
174+
175+
// Read team.
176+
teamRead = unittest.AssertExistsAndLoadBean(t, &models.Team{ID: teamID}).(*models.Team)
177+
req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
178+
resp = session.MakeRequest(t, req, http.StatusOK)
179+
apiTeam = api.Team{}
180+
DecodeJSON(t, resp, &apiTeam)
181+
assert.NoError(t, teamRead.GetUnits())
182+
checkTeamResponse(t, &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
183+
teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
112184

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

119-
func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string) {
120-
assert.Equal(t, name, apiTeam.Name, "name")
121-
assert.Equal(t, description, apiTeam.Description, "description")
122-
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
123-
assert.Equal(t, permission, apiTeam.Permission, "permission")
124-
sort.StringSlice(units).Sort()
125-
sort.StringSlice(apiTeam.Units).Sort()
126-
assert.EqualValues(t, units, apiTeam.Units, "units")
191+
func checkTeamResponse(t *testing.T, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
192+
t.Run(name+description, func(t *testing.T) {
193+
assert.Equal(t, name, apiTeam.Name, "name")
194+
assert.Equal(t, description, apiTeam.Description, "description")
195+
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
196+
assert.Equal(t, permission, apiTeam.Permission, "permission")
197+
if units != nil {
198+
sort.StringSlice(units).Sort()
199+
sort.StringSlice(apiTeam.Units).Sort()
200+
assert.EqualValues(t, units, apiTeam.Units, "units")
201+
}
202+
if unitsMap != nil {
203+
assert.EqualValues(t, unitsMap, apiTeam.UnitsMap, "unitsMap")
204+
}
205+
})
127206
}
128207

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

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

integrations/org_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ func TestOrgRestrictedUser(t *testing.T) {
156156
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
157157
DecodeJSON(t, resp, &apiTeam)
158158
checkTeamResponse(t, &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
159-
teamToCreate.Permission, teamToCreate.Units)
159+
teamToCreate.Permission, teamToCreate.Units, nil)
160160
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
161-
teamToCreate.Permission, teamToCreate.Units)
162-
//teamID := apiTeam.ID
161+
teamToCreate.Permission, teamToCreate.Units, nil)
162+
// teamID := apiTeam.ID
163163

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

173173
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName))
174174
restrictedSession.MakeRequest(t, req, http.StatusOK)
175-
176175
}

models/access.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func recalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
162162
// Owner team gets owner access, and skip for teams that do not
163163
// have relations with repository.
164164
if t.IsOwnerTeam() {
165-
t.Authorize = perm.AccessModeOwner
165+
t.AccessMode = perm.AccessModeOwner
166166
} else if !t.hasRepository(e, repo.ID) {
167167
continue
168168
}
@@ -171,7 +171,7 @@ func recalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
171171
return fmt.Errorf("getMembers '%d': %v", t.ID, err)
172172
}
173173
for _, m := range t.Members {
174-
updateUserAccess(accessMap, m, t.Authorize)
174+
updateUserAccess(accessMap, m, t.AccessMode)
175175
}
176176
}
177177

@@ -210,10 +210,10 @@ func recalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid
210210

211211
for _, t := range teams {
212212
if t.IsOwnerTeam() {
213-
t.Authorize = perm.AccessModeOwner
213+
t.AccessMode = perm.AccessModeOwner
214214
}
215215

216-
accessMode = maxAccessMode(accessMode, t.Authorize)
216+
accessMode = maxAccessMode(accessMode, t.AccessMode)
217217
}
218218
}
219219

0 commit comments

Comments
 (0)