Skip to content

Commit

Permalink
Team permission allow different unit has different permission (#17811)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
4 people committed Jan 5, 2022
1 parent 12ad6dd commit 8760af7
Show file tree
Hide file tree
Showing 27 changed files with 610 additions and 170 deletions.
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)

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

}
8 changes: 4 additions & 4 deletions models/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func recalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
// Owner team gets owner access, and skip for teams that do not
// have relations with repository.
if t.IsOwnerTeam() {
t.Authorize = perm.AccessModeOwner
t.AccessMode = perm.AccessModeOwner
} else if !t.hasRepository(e, repo.ID) {
continue
}
Expand All @@ -171,7 +171,7 @@ func recalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, i
return fmt.Errorf("getMembers '%d': %v", t.ID, err)
}
for _, m := range t.Members {
updateUserAccess(accessMap, m, t.Authorize)
updateUserAccess(accessMap, m, t.AccessMode)
}
}

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

for _, t := range teams {
if t.IsOwnerTeam() {
t.Authorize = perm.AccessModeOwner
t.AccessMode = perm.AccessModeOwner
}

accessMode = maxAccessMode(accessMode, t.Authorize)
accessMode = maxAccessMode(accessMode, t.AccessMode)
}
}

Expand Down
Loading

0 comments on commit 8760af7

Please sign in to comment.