diff --git a/docs/content/doc/usage/permissions.en-us.md b/docs/content/doc/usage/permissions.en-us.md new file mode 100644 index 0000000000000..1eea78b557134 --- /dev/null +++ b/docs/content/doc/usage/permissions.en-us.md @@ -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. diff --git a/integrations/api_repo_teams_test.go b/integrations/api_repo_teams_test.go index 07a8b9418e1eb..a3baeba63c804 100644 --- a/integrations/api_repo_teams_test.go +++ b/integrations/api_repo_teams_test.go @@ -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" ) @@ -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) diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go index da22d40479760..a622c63145f70 100644 --- a/integrations/api_team_test.go +++ b/integrations/api_team_test.go @@ -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" @@ -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. @@ -85,30 +87,100 @@ 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) @@ -116,20 +188,27 @@ func TestAPITeam(t *testing.T) { 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 { @@ -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) - } diff --git a/integrations/dump_restore_test.go b/integrations/dump_restore_test.go new file mode 100644 index 0000000000000..6fe5d8fe6a86c --- /dev/null +++ b/integrations/dump_restore_test.go @@ -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)) + }) +} diff --git a/integrations/org_test.go b/integrations/org_test.go index e94e4ea74c1c3..794475a9245d8 100644 --- a/integrations/org_test.go +++ b/integrations/org_test.go @@ -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", @@ -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) - } diff --git a/models/access.go b/models/access.go index 6a97bcffcf7b5..48b65c2c0f605 100644 --- a/models/access.go +++ b/models/access.go @@ -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 } @@ -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) } } @@ -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) } } diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index 943745c000f9b..66f0d22efdfe5 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -2,223 +2,268 @@ id: 1 team_id: 1 type: 1 + access_mode: 4 - id: 2 team_id: 1 type: 2 + access_mode: 4 - id: 3 team_id: 1 type: 3 + access_mode: 4 - id: 4 team_id: 1 type: 4 + access_mode: 4 - id: 5 team_id: 1 type: 5 + access_mode: 4 - id: 6 team_id: 1 type: 6 + access_mode: 4 - id: 7 team_id: 1 type: 7 + access_mode: 4 - id: 8 team_id: 2 type: 1 + access_mode: 2 - id: 9 team_id: 2 type: 2 + access_mode: 2 - id: 10 team_id: 2 type: 3 + access_mode: 2 - id: 11 team_id: 2 type: 4 + access_mode: 2 - id: 12 team_id: 2 type: 5 + access_mode: 2 - id: 13 team_id: 2 type: 6 + access_mode: 2 - id: 14 team_id: 2 type: 7 + access_mode: 2 - id: 15 team_id: 3 type: 1 + access_mode: 4 - id: 16 team_id: 3 type: 2 + access_mode: 4 - id: 17 team_id: 3 type: 3 + access_mode: 4 - id: 18 team_id: 3 type: 4 + access_mode: 4 - id: 19 team_id: 3 type: 5 + access_mode: 4 - id: 20 team_id: 3 type: 6 + access_mode: 4 - id: 21 team_id: 3 type: 7 + access_mode: 4 - id: 22 team_id: 4 type: 1 + access_mode: 4 - id: 23 team_id: 4 type: 2 + access_mode: 4 - id: 24 team_id: 4 type: 3 + access_mode: 4 - id: 25 team_id: 4 type: 4 + access_mode: 4 - id: 26 team_id: 4 type: 5 + access_mode: 4 - id: 27 team_id: 4 type: 6 + access_mode: 4 - id: 28 team_id: 4 type: 7 + access_mode: 4 - id: 29 team_id: 5 type: 1 + access_mode: 4 - id: 30 team_id: 5 type: 2 + access_mode: 4 - id: 31 team_id: 5 type: 3 + access_mode: 4 - id: 32 team_id: 5 type: 4 + access_mode: 4 - id: 33 team_id: 5 type: 5 + access_mode: 4 - id: 34 team_id: 5 type: 6 + access_mode: 4 - id: 35 team_id: 5 type: 7 + access_mode: 4 - id: 36 team_id: 6 type: 1 + access_mode: 4 - id: 37 team_id: 6 type: 2 + access_mode: 4 - id: 38 team_id: 6 type: 3 + access_mode: 4 - id: 39 team_id: 6 type: 4 + access_mode: 4 - id: 40 team_id: 6 type: 5 + access_mode: 4 - id: 41 team_id: 6 type: 6 + access_mode: 4 - id: 42 team_id: 6 type: 7 + access_mode: 4 - id: 43 team_id: 7 type: 2 # issues + access_mode: 2 - id: 44 team_id: 8 type: 2 # issues + access_mode: 2 - id: 45 team_id: 9 - type: 1 # code \ No newline at end of file + type: 1 # code + access_mode: 1 diff --git a/models/issue.go b/models/issue.go index f0040fbbc1af4..108d9b217afd8 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1350,8 +1350,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { // issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *Organization, team *Team, isPull bool) builder.Cond { - var cond = builder.NewCond() - var unitType = unit.TypeIssues + cond := builder.NewCond() + unitType := unit.TypeIssues if isPull { unitType = unit.TypePullRequests } @@ -2147,7 +2147,7 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx context.Context, doer *user_ unittype = unit.TypePullRequests } for _, team := range teams { - if team.Authorize >= perm.AccessModeOwner { + if team.AccessMode >= perm.AccessModeAdmin { checked = append(checked, team.ID) resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true continue diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4b720c3f02a4f..9423e5c5f6954 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -60,7 +60,6 @@ type Version struct { // If you want to "retire" a migration, remove it from the top of the list and // update minDBVersion accordingly var migrations = []Migration{ - // Gitea 1.5.0 ends at v69 // v70 -> v71 @@ -365,6 +364,8 @@ var migrations = []Migration{ NewMigration("Add key is verified to ssh key", addSSHKeyIsVerified), // v205 -> v206 NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt), + // v206 -> v207 + NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v206.go b/models/migrations/v206.go new file mode 100644 index 0000000000000..c6a5dc811c59b --- /dev/null +++ b/models/migrations/v206.go @@ -0,0 +1,29 @@ +// 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 migrations + +import ( + "fmt" + + "xorm.io/xorm" +) + +func addAuthorizeColForTeamUnit(x *xorm.Engine) error { + type TeamUnit struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type int `xorm:"UNIQUE(s)"` + AccessMode int + } + + if err := x.Sync2(new(TeamUnit)); err != nil { + return fmt.Errorf("sync2: %v", err) + } + + // migrate old permission + _, err := x.Exec("UPDATE team_unit SET access_mode = (SELECT authorize FROM team WHERE team.id = team_unit.team_id)") + return err +} diff --git a/models/org.go b/models/org.go index c135bb9d3cc31..0ea2ce6886fd2 100644 --- a/models/org.go +++ b/models/org.go @@ -265,7 +265,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) { OrgID: org.ID, LowerName: strings.ToLower(ownerTeamName), Name: ownerTeamName, - Authorize: perm.AccessModeOwner, + AccessMode: perm.AccessModeOwner, NumMembers: 1, IncludesAllRepositories: true, CanCreateOrgRepo: true, @@ -523,7 +523,7 @@ type FindOrgOptions struct { } func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { - var cond = builder.Eq{"uid": userID} + cond := builder.Eq{"uid": userID} if !includePrivate { cond["is_public"] = true } @@ -531,7 +531,7 @@ func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { } func (opts FindOrgOptions) toConds() builder.Cond { - var cond = builder.NewCond() + cond := builder.NewCond() if opts.UserID > 0 { cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) } diff --git a/models/org_team.go b/models/org_team.go index 7eac0f7bc52fe..bce4afb0611b1 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -32,7 +32,7 @@ type Team struct { LowerName string Name string Description string - Authorize perm.AccessMode + AccessMode perm.AccessMode `xorm:"'authorize'"` Repos []*repo_model.Repository `xorm:"-"` Members []*user_model.User `xorm:"-"` NumRepos int @@ -126,7 +126,7 @@ func (t *Team) ColorFormat(s fmt.State) { log.NewColoredIDValue(t.ID), t.Name, log.NewColoredIDValue(t.OrgID), - t.Authorize) + t.AccessMode) } // GetUnits return a list of available units for a team @@ -145,15 +145,29 @@ func (t *Team) getUnits(e db.Engine) (err error) { // GetUnitNames returns the team units names func (t *Team) GetUnitNames() (res []string) { + if t.AccessMode >= perm.AccessModeAdmin { + return unit.AllUnitKeyNames() + } + for _, u := range t.Units { res = append(res, unit.Units[u.Type].NameKey) } return } -// HasWriteAccess returns true if team has at least write level access mode. -func (t *Team) HasWriteAccess() bool { - return t.Authorize >= perm.AccessModeWrite +// GetUnitsMap returns the team units permissions +func (t *Team) GetUnitsMap() map[string]string { + m := make(map[string]string) + if t.AccessMode >= perm.AccessModeAdmin { + for _, u := range unit.Units { + m[u.NameKey] = t.AccessMode.String() + } + } else { + for _, u := range t.Units { + m[u.Unit().NameKey] = u.AccessMode.String() + } + } + return m } // IsOwnerTeam returns true if team is owner team. @@ -455,16 +469,25 @@ func (t *Team) UnitEnabled(tp unit.Type) bool { } func (t *Team) unitEnabled(e db.Engine, tp unit.Type) bool { + return t.unitAccessMode(e, tp) > perm.AccessModeNone +} + +// UnitAccessMode returns if the team has the given unit type enabled +func (t *Team) UnitAccessMode(tp unit.Type) perm.AccessMode { + return t.unitAccessMode(db.GetEngine(db.DefaultContext), tp) +} + +func (t *Team) unitAccessMode(e db.Engine, tp unit.Type) perm.AccessMode { if err := t.getUnits(e); err != nil { log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error()) } for _, unit := range t.Units { if unit.Type == tp { - return true + return unit.AccessMode } } - return false + return perm.AccessModeNone } // IsUsableTeamName tests if a name could be as team name @@ -661,7 +684,7 @@ func UpdateTeam(t *Team, authChanged, includeAllChanged bool) (err error) { Delete(new(TeamUnit)); err != nil { return err } - if _, err = sess.Cols("org_id", "team_id", "type").Insert(&t.Units); err != nil { + if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil { return err } } @@ -1033,10 +1056,11 @@ func GetTeamsWithAccessToRepo(orgID, repoID int64, mode perm.AccessMode) ([]*Tea // TeamUnit describes all units of a repository type TeamUnit struct { - ID int64 `xorm:"pk autoincr"` - OrgID int64 `xorm:"INDEX"` - TeamID int64 `xorm:"UNIQUE(s)"` - Type unit.Type `xorm:"UNIQUE(s)"` + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type unit.Type `xorm:"UNIQUE(s)"` + AccessMode perm.AccessMode } // Unit returns Unit diff --git a/models/org_team_test.go b/models/org_team_test.go index 59b7b6d5a834c..aa62cc58e2d00 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -211,7 +211,7 @@ func TestUpdateTeam(t *testing.T) { team.LowerName = "newname" team.Name = "newName" team.Description = strings.Repeat("A long description!", 100) - team.Authorize = perm.AccessModeAdmin + team.AccessMode = perm.AccessModeAdmin assert.NoError(t, UpdateTeam(team, true, false)) team = unittest.AssertExistsAndLoadBean(t, &Team{Name: "newName"}).(*Team) diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go index f2c0a322a085f..dfa7f7b7524a1 100644 --- a/models/perm/access_mode.go +++ b/models/perm/access_mode.go @@ -51,11 +51,13 @@ func (mode AccessMode) ColorFormat(s fmt.State) { // ParseAccessMode returns corresponding access mode to given permission string. func ParseAccessMode(permission string) AccessMode { switch permission { + case "read": + return AccessModeRead case "write": return AccessModeWrite case "admin": return AccessModeAdmin default: - return AccessModeRead + return AccessModeNone } } diff --git a/models/repo_permission.go b/models/repo_permission.go index 40b63aa804313..4e5cbfd55807e 100644 --- a/models/repo_permission.go +++ b/models/repo_permission.go @@ -239,7 +239,7 @@ func getUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use // if user in an owner team for _, team := range teams { - if team.Authorize >= perm_model.AccessModeOwner { + if team.AccessMode >= perm_model.AccessModeAdmin { perm.AccessMode = perm_model.AccessModeOwner perm.UnitsMode = nil return @@ -249,10 +249,11 @@ func getUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use for _, u := range repo.Units { var found bool for _, team := range teams { - if team.unitEnabled(e, u.Type) { + teamMode := team.unitAccessMode(e, u.Type) + if teamMode > perm_model.AccessModeNone { m := perm.UnitsMode[u.Type] - if m < team.Authorize { - perm.UnitsMode[u.Type] = team.Authorize + if m < teamMode { + perm.UnitsMode[u.Type] = teamMode } found = true } @@ -324,7 +325,7 @@ func isUserRepoAdmin(e db.Engine, repo *repo_model.Repository, user *user_model. } for _, team := range teams { - if team.Authorize >= perm_model.AccessModeAdmin { + if team.AccessMode >= perm_model.AccessModeAdmin { return true, nil } } diff --git a/models/review.go b/models/review.go index eeb33611ceb15..023f98c3eace5 100644 --- a/models/review.go +++ b/models/review.go @@ -280,7 +280,7 @@ func isOfficialReviewerTeam(ctx context.Context, issue *Issue, team *Team) (bool } if !pr.ProtectedBranch.EnableApprovalsWhitelist { - return team.Authorize >= perm.AccessModeWrite, nil + return team.UnitAccessMode(unit.TypeCode) >= perm.AccessModeWrite, nil } return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil diff --git a/models/unit/unit.go b/models/unit/unit.go index 0af4640b7a4b4..b05f34b64cca6 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -17,14 +18,15 @@ type Type int // Enumerate all the unit types const ( - TypeCode Type = iota + 1 // 1 code - TypeIssues // 2 issues - TypePullRequests // 3 PRs - TypeReleases // 4 Releases - TypeWiki // 5 Wiki - TypeExternalWiki // 6 ExternalWiki - TypeExternalTracker // 7 ExternalTracker - TypeProjects // 8 Kanban board + TypeInvalid Type = iota // 0 invalid + TypeCode // 1 code + TypeIssues // 2 issues + TypePullRequests // 3 PRs + TypeReleases // 4 Releases + TypeWiki // 5 Wiki + TypeExternalWiki // 6 ExternalWiki + TypeExternalTracker // 7 ExternalTracker + TypeProjects // 8 Kanban board ) // Value returns integer value for unit type @@ -170,11 +172,12 @@ func (u *Type) CanBeDefault() bool { // Unit is a section of one repository type Unit struct { - Type Type - NameKey string - URI string - DescKey string - Idx int + Type Type + NameKey string + URI string + DescKey string + Idx int + MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read. } // CanDisable returns if this unit could be disabled. @@ -198,6 +201,7 @@ var ( "/", "repo.code.desc", 0, + perm.AccessModeOwner, } UnitIssues = Unit{ @@ -206,6 +210,7 @@ var ( "/issues", "repo.issues.desc", 1, + perm.AccessModeOwner, } UnitExternalTracker = Unit{ @@ -214,6 +219,7 @@ var ( "/issues", "repo.ext_issues.desc", 1, + perm.AccessModeRead, } UnitPullRequests = Unit{ @@ -222,6 +228,7 @@ var ( "/pulls", "repo.pulls.desc", 2, + perm.AccessModeOwner, } UnitReleases = Unit{ @@ -230,6 +237,7 @@ var ( "/releases", "repo.releases.desc", 3, + perm.AccessModeOwner, } UnitWiki = Unit{ @@ -238,6 +246,7 @@ var ( "/wiki", "repo.wiki.desc", 4, + perm.AccessModeOwner, } UnitExternalWiki = Unit{ @@ -246,6 +255,7 @@ var ( "/wiki", "repo.ext_wiki.desc", 4, + perm.AccessModeRead, } UnitProjects = Unit{ @@ -254,6 +264,7 @@ var ( "/projects", "repo.projects.desc", 5, + perm.AccessModeOwner, } // Units contains all the units @@ -269,15 +280,51 @@ var ( } ) -// FindUnitTypes give the unit key name and return unit +// FindUnitTypes give the unit key names and return unit func FindUnitTypes(nameKeys ...string) (res []Type) { for _, key := range nameKeys { + var found bool for t, u := range Units { if strings.EqualFold(key, u.NameKey) { res = append(res, t) + found = true break } } + if !found { + res = append(res, TypeInvalid) + } } return } + +// TypeFromKey give the unit key name and return unit +func TypeFromKey(nameKey string) Type { + for t, u := range Units { + if strings.EqualFold(nameKey, u.NameKey) { + return t + } + } + return TypeInvalid +} + +// AllUnitKeyNames returns all unit key names +func AllUnitKeyNames() []string { + res := make([]string, 0, len(Units)) + for _, u := range Units { + res = append(res, u.NameKey) + } + return res +} + +// MinUnitAccessMode returns the minial permission of the permission map +func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode { + res := perm.AccessModeNone + for _, mode := range unitsMap { + // get the minial permission great than AccessModeNone except all are AccessModeNone + if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) { + res = mode + } + } + return res +} diff --git a/modules/context/org.go b/modules/context/org.go index eb81f6644c171..585a5fd762c6d 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -168,7 +168,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } - ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize >= perm.AccessModeAdmin + ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin if requireTeamAdmin && !ctx.Org.IsTeamAdmin { ctx.NotFound("OrgAssignment", err) diff --git a/modules/convert/convert.go b/modules/convert/convert.go index f2b62a74bf7c9..41a044c6d74e2 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -306,8 +306,9 @@ func ToTeam(team *models.Team) *api.Team { Description: team.Description, IncludesAllRepositories: team.IncludesAllRepositories, CanCreateOrgRepo: team.CanCreateOrgRepo, - Permission: team.Authorize.String(), + Permission: team.AccessMode.String(), Units: team.GetUnitNames(), + UnitsMap: team.GetUnitsMap(), } } diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index 18995f4ecd210..ed890ace43319 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -70,25 +70,25 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { { OrgID: org.ID, Name: "team one", - Authorize: perm.AccessModeRead, + AccessMode: perm.AccessModeRead, IncludesAllRepositories: true, }, { OrgID: org.ID, Name: "team 2", - Authorize: perm.AccessModeRead, + AccessMode: perm.AccessModeRead, IncludesAllRepositories: false, }, { OrgID: org.ID, Name: "team three", - Authorize: perm.AccessModeWrite, + AccessMode: perm.AccessModeWrite, IncludesAllRepositories: true, }, { OrgID: org.ID, Name: "team 4", - Authorize: perm.AccessModeWrite, + AccessMode: perm.AccessModeWrite, IncludesAllRepositories: false, }, } diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index 3b2c5e78391e0..53e3fcf62da45 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -15,8 +15,11 @@ type Team struct { // 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.projects","repo.ext_wiki"] - Units []string `json:"units"` - CanCreateOrgRepo bool `json:"can_create_org_repo"` + // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. + Units []string `json:"units"` + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"] + UnitsMap map[string]string `json:"units_map"` + CanCreateOrgRepo bool `json:"can_create_org_repo"` } // CreateTeamOption options for creating a team @@ -28,8 +31,11 @@ type CreateTeamOption struct { // enum: read,write,admin Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] - Units []string `json:"units"` - CanCreateOrgRepo bool `json:"can_create_org_repo"` + // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. + Units []string `json:"units"` + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"] + UnitsMap map[string]string `json:"units_map"` + CanCreateOrgRepo bool `json:"can_create_org_repo"` } // EditTeamOption options for editing a team @@ -41,6 +47,9 @@ type EditTeamOption struct { // enum: read,write,admin Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] - Units []string `json:"units"` - CanCreateOrgRepo *bool `json:"can_create_org_repo"` + // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. + Units []string `json:"units"` + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"] + UnitsMap map[string]string `json:"units_map"` + CanCreateOrgRepo *bool `json:"can_create_org_repo"` } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9164d5ffdceda..7a3dbd50a812c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1099,7 +1099,7 @@ commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does n commits.gpg_key_id = GPG Key ID commits.ssh_key_fingerprint = SSH Key Fingerprint -ext_issues = Ext. Issues +ext_issues = Access to External Issues ext_issues.desc = Link to an external issue tracker. projects = Projects @@ -1579,7 +1579,7 @@ signing.wont_sign.commitssigned = The merge will not be signed as all the associ signing.wont_sign.approved = The merge will not be signed as the PR is not approved signing.wont_sign.not_signed_in = You are not signed in -ext_wiki = Ext. Wiki +ext_wiki = Access to External Wiki ext_wiki.desc = Link to an external wiki. wiki = Wiki @@ -2261,9 +2261,13 @@ teams.leave = Leave teams.leave.detail = Leave %s? 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.none_access = No Access +teams.none_access_helper = Members cannot view or do any other action on this unit. +teams.general_access = General Access +teams.general_access_helper = Members permissions will be decided by below permission table. +teams.read_access = Read teams.read_access_helper = Members can view and clone team repositories. -teams.write_access = Write Access +teams.write_access = Write teams.write_access_helper = Members can read and push to team repositories. teams.admin_access = Administrator Access teams.admin_access_helper = Members can pull and push to team repositories and add collaborators to them. @@ -2892,5 +2896,6 @@ error.probable_bad_signature = "WARNING! Although there is a key with this ID in error.probable_bad_default_signature = "WARNING! Although the default key has this ID it does not verify this commit! This commit is SUSPICIOUS." [units] +unit = Unit error.no_unit_allowed_repo = You are not allowed to access any section of this repository. error.unit_not_allowed = You are not allowed to access this repository section. diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index d39125b0500e3..cc7a63af337ec 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -6,6 +6,7 @@ package org import ( + "errors" "net/http" "code.gitea.io/gitea/models" @@ -50,7 +51,6 @@ func ListTeams(ctx *context.APIContext) { ListOptions: utils.GetListOptions(ctx), OrgID: ctx.Org.Organization.ID, }) - if err != nil { ctx.Error(http.StatusInternalServerError, "LoadTeams", err) return @@ -112,6 +112,10 @@ func ListUserTeams(ctx *context.APIContext) { apiOrg = convert.ToOrganization(org) cache[teams[i].OrgID] = apiOrg } + if err := teams[i].GetUnits(); err != nil { + ctx.Error(http.StatusInternalServerError, "teams[i].GetUnits()", err) + return + } apiTeams[i] = convert.ToTeam(teams[i]) apiTeams[i].Organization = apiOrg } @@ -138,9 +142,45 @@ func GetTeam(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Team" + if err := ctx.Org.Team.GetUnits(); err != nil { + ctx.Error(http.StatusInternalServerError, "team.GetUnits", err) + return + } + ctx.JSON(http.StatusOK, convert.ToTeam(ctx.Org.Team)) } +func attachTeamUnits(team *models.Team, units []string) { + unitTypes := unit_model.FindUnitTypes(units...) + team.Units = make([]*models.TeamUnit, 0, len(units)) + for _, tp := range unitTypes { + team.Units = append(team.Units, &models.TeamUnit{ + OrgID: team.OrgID, + Type: tp, + AccessMode: team.AccessMode, + }) + } +} + +func convertUnitsMap(unitsMap map[string]string) map[unit_model.Type]perm.AccessMode { + res := make(map[unit_model.Type]perm.AccessMode, len(unitsMap)) + for unitKey, p := range unitsMap { + res[unit_model.TypeFromKey(unitKey)] = perm.ParseAccessMode(p) + } + return res +} + +func attachTeamUnitsMap(team *models.Team, unitsMap map[string]string) { + team.Units = make([]*models.TeamUnit, 0, len(unitsMap)) + for unitKey, p := range unitsMap { + team.Units = append(team.Units, &models.TeamUnit{ + OrgID: team.OrgID, + Type: unit_model.TypeFromKey(unitKey), + AccessMode: perm.ParseAccessMode(p), + }) + } +} + // CreateTeam api for create a team func CreateTeam(ctx *context.APIContext) { // swagger:operation POST /orgs/{org}/teams organization orgCreateTeam @@ -166,26 +206,28 @@ func CreateTeam(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateTeamOption) + p := perm.ParseAccessMode(form.Permission) + if p < perm.AccessModeAdmin && len(form.UnitsMap) > 0 { + p = unit_model.MinUnitAccessMode(convertUnitsMap(form.UnitsMap)) + } team := &models.Team{ OrgID: ctx.Org.Organization.ID, Name: form.Name, Description: form.Description, IncludesAllRepositories: form.IncludesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, - Authorize: perm.ParseAccessMode(form.Permission), + AccessMode: p, } - unitTypes := unit_model.FindUnitTypes(form.Units...) - - if team.Authorize < perm.AccessModeOwner { - var units = make([]*models.TeamUnit, 0, len(form.Units)) - for _, tp := range unitTypes { - units = append(units, &models.TeamUnit{ - OrgID: ctx.Org.Organization.ID, - Type: tp, - }) + if team.AccessMode < perm.AccessModeAdmin { + if len(form.UnitsMap) > 0 { + attachTeamUnitsMap(team, form.UnitsMap) + } else if len(form.Units) > 0 { + attachTeamUnits(team, form.Units) + } else { + ctx.Error(http.StatusInternalServerError, "getTeamUnits", errors.New("units permission should not be empty")) + return } - team.Units = units } if err := models.NewTeam(team); err != nil { @@ -224,7 +266,6 @@ func EditTeam(ctx *context.APIContext) { // "$ref": "#/responses/Team" form := web.GetForm(ctx).(*api.EditTeamOption) - team := ctx.Org.Team if err := team.GetUnits(); err != nil { ctx.InternalServerError(err) @@ -247,11 +288,14 @@ func EditTeam(ctx *context.APIContext) { isIncludeAllChanged := false if !team.IsOwnerTeam() && len(form.Permission) != 0 { // Validate permission level. - auth := perm.ParseAccessMode(form.Permission) + p := perm.ParseAccessMode(form.Permission) + if p < perm.AccessModeAdmin && len(form.UnitsMap) > 0 { + p = unit_model.MinUnitAccessMode(convertUnitsMap(form.UnitsMap)) + } - if team.Authorize != auth { + if team.AccessMode != p { isAuthChanged = true - team.Authorize = auth + team.AccessMode = p } if form.IncludesAllRepositories != nil { @@ -260,17 +304,11 @@ func EditTeam(ctx *context.APIContext) { } } - if team.Authorize < perm.AccessModeOwner { - if len(form.Units) > 0 { - var units = make([]*models.TeamUnit, 0, len(form.Units)) - unitTypes := unit_model.FindUnitTypes(form.Units...) - for _, tp := range unitTypes { - units = append(units, &models.TeamUnit{ - OrgID: ctx.Org.Team.OrgID, - Type: tp, - }) - } - team.Units = units + if team.AccessMode < perm.AccessModeAdmin { + if len(form.UnitsMap) > 0 { + attachTeamUnitsMap(team, form.UnitsMap) + } else if len(form.Units) > 0 { + attachTeamUnits(team, form.Units) } } @@ -706,5 +744,4 @@ func SearchTeam(ctx *context.APIContext) { "ok": true, "data": apiTeams, }) - } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 40fba5cd09a4f..732f24b22c27d 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "code.gitea.io/gitea/models" @@ -224,35 +225,57 @@ func NewTeam(ctx *context.Context) { ctx.HTML(http.StatusOK, tplTeamNew) } +func getUnitPerms(forms url.Values) map[unit_model.Type]perm.AccessMode { + unitPerms := make(map[unit_model.Type]perm.AccessMode) + for k, v := range forms { + if strings.HasPrefix(k, "unit_") { + t, _ := strconv.Atoi(k[5:]) + if t > 0 { + vv, _ := strconv.Atoi(v[0]) + unitPerms[unit_model.Type(t)] = perm.AccessMode(vv) + } + } + } + return unitPerms +} + // NewTeamPost response for create new team func NewTeamPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateTeamForm) - ctx.Data["Title"] = ctx.Org.Organization.FullName - ctx.Data["PageIsOrgTeams"] = true - ctx.Data["PageIsOrgTeamsNew"] = true - ctx.Data["Units"] = unit_model.Units - var includesAllRepositories = form.RepoAccess == "all" + includesAllRepositories := form.RepoAccess == "all" + unitPerms := getUnitPerms(ctx.Req.Form) + p := perm.ParseAccessMode(form.Permission) + if p < perm.AccessModeAdmin { + // if p is less than admin accessmode, then it should be general accessmode, + // so we should calculate the minial accessmode from units accessmodes. + p = unit_model.MinUnitAccessMode(unitPerms) + } t := &models.Team{ OrgID: ctx.Org.Organization.ID, Name: form.TeamName, Description: form.Description, - Authorize: perm.ParseAccessMode(form.Permission), + AccessMode: p, IncludesAllRepositories: includesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, } - if t.Authorize < perm.AccessModeOwner { - var units = make([]*models.TeamUnit, 0, len(form.Units)) - for _, tp := range form.Units { + if t.AccessMode < perm.AccessModeAdmin { + units := make([]*models.TeamUnit, 0, len(unitPerms)) + for tp, perm := range unitPerms { units = append(units, &models.TeamUnit{ - OrgID: ctx.Org.Organization.ID, - Type: tp, + OrgID: ctx.Org.Organization.ID, + Type: tp, + AccessMode: perm, }) } t.Units = units } + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamsNew"] = true + ctx.Data["Units"] = unit_model.Units ctx.Data["Team"] = t if ctx.HasError() { @@ -260,7 +283,7 @@ func NewTeamPost(ctx *context.Context) { return } - if t.Authorize < perm.AccessModeAdmin && len(form.Units) == 0 { + if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } @@ -317,22 +340,29 @@ func EditTeam(ctx *context.Context) { func EditTeamPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateTeamForm) t := ctx.Org.Team + unitPerms := getUnitPerms(ctx.Req.Form) + isAuthChanged := false + isIncludeAllChanged := false + includesAllRepositories := form.RepoAccess == "all" + ctx.Data["Title"] = ctx.Org.Organization.FullName ctx.Data["PageIsOrgTeams"] = true ctx.Data["Team"] = t ctx.Data["Units"] = unit_model.Units - isAuthChanged := false - isIncludeAllChanged := false - var includesAllRepositories = form.RepoAccess == "all" if !t.IsOwnerTeam() { // Validate permission level. - auth := perm.ParseAccessMode(form.Permission) + newAccessMode := perm.ParseAccessMode(form.Permission) + if newAccessMode < perm.AccessModeAdmin { + // if p is less than admin accessmode, then it should be general accessmode, + // so we should calculate the minial accessmode from units accessmodes. + newAccessMode = unit_model.MinUnitAccessMode(unitPerms) + } t.Name = form.TeamName - if t.Authorize != auth { + if t.AccessMode != newAccessMode { isAuthChanged = true - t.Authorize = auth + t.AccessMode = newAccessMode } if t.IncludesAllRepositories != includesAllRepositories { @@ -341,17 +371,17 @@ func EditTeamPost(ctx *context.Context) { } } t.Description = form.Description - if t.Authorize < perm.AccessModeOwner { - var units = make([]models.TeamUnit, 0, len(form.Units)) - for _, tp := range form.Units { + if t.AccessMode < perm.AccessModeAdmin { + units := make([]models.TeamUnit, 0, len(unitPerms)) + for tp, perm := range unitPerms { units = append(units, models.TeamUnit{ - OrgID: t.OrgID, - TeamID: t.ID, - Type: tp, + OrgID: t.OrgID, + TeamID: t.ID, + Type: tp, + AccessMode: perm, }) } - err := models.UpdateTeamUnits(t, units) - if err != nil { + if err := models.UpdateTeamUnits(t, units); err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err.Error()) return } @@ -363,7 +393,7 @@ func EditTeamPost(ctx *context.Context) { return } - if t.Authorize < perm.AccessModeAdmin && len(form.Units) == 0 { + if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } diff --git a/services/forms/org.go b/services/forms/org.go index 7c8f17f95ee60..dec2dbfa6555a 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -8,7 +8,6 @@ package forms import ( "net/http" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" @@ -66,7 +65,6 @@ type CreateTeamForm struct { TeamName string `binding:"Required;AlphaDashDot;MaxSize(30)"` Description string `binding:"MaxSize(255)"` Permission string - Units []unit.Type RepoAccess string CanCreateOrgRepo bool } diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index 783e025ebdc08..1cf2dd0236ec1 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -56,21 +56,14 @@
- - - {{.i18n.Tr "org.teams.read_access_helper"}} + + + {{.i18n.Tr "org.teams.general_access_helper"}}
- - - {{.i18n.Tr "org.teams.write_access_helper"}} -
-
-
-
- + {{.i18n.Tr "org.teams.admin_access_helper"}}
@@ -78,24 +71,66 @@
-