diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3a26784ae1bf..a58e52cb4163 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1496,6 +1496,9 @@ PATH = ;; ;; Timeout for Sendmail ;SENDMAIL_TIMEOUT = 5m +;; +;; convert \r\n to \n for Sendmail +;SENDMAIL_CONVERT_CRLF = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index f4f8798f1b06..dfafa84175ef 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -667,6 +667,7 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type command or full path). - `SENDMAIL_ARGS`: **_empty_**: Specify any extra sendmail arguments. - `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail +- `SENDMAIL_CONVERT_CRLF`: **true**: Most versions of sendmail prefer LF line endings rather than CRLF line endings. Set this to false if your version of sendmail requires CRLF line endings. - `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]` ## Cache (`cache`) 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 000000000000..1eea78b55713 --- /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/go.mod b/go.mod index 070ab653251f..27e89fae3c17 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( cloud.google.com/go v0.78.0 // indirect code.gitea.io/gitea-vet v0.2.1 - code.gitea.io/sdk/gitea v0.14.0 + code.gitea.io/sdk/gitea v0.15.1 gitea.com/go-chi/binding v0.0.0-20211013065440-d16dc407c2be gitea.com/go-chi/cache v0.0.0-20211013020926-78790b11abf1 gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5 diff --git a/go.sum b/go.sum index c9c37e70c0ae..fd1105627e58 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0= -code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= +code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= +code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gitea.com/go-chi/binding v0.0.0-20211013065440-d16dc407c2be h1:IzSwPVzd2hE6e67ujY8ReBCrQ5IFNd0uiBmC7Ux5IaY= gitea.com/go-chi/binding v0.0.0-20211013065440-d16dc407c2be/go.mod h1:/vR0YjlusOYvosKYW7QKcSnrY0nPLe4RQ/DGi3+i/Do= diff --git a/integrations/api_repo_teams_test.go b/integrations/api_repo_teams_test.go index 07a8b9418e1e..a3baeba63c80 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 da22d4047976..a622c63145f7 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/org_test.go b/integrations/org_test.go index e94e4ea74c1c..794475a9245d 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 6a97bcffcf7b..48b65c2c0f60 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/avatars/avatar.go b/models/avatars/avatar.go index 7206a8ae9052..6107856adeed 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -10,6 +10,7 @@ import ( "path" "strconv" "strings" + "sync" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/base" @@ -31,16 +32,24 @@ func init() { db.RegisterModel(new(EmailHash)) } +var ( + defaultAvatarLink string + once sync.Once +) + // DefaultAvatarLink the default avatar link func DefaultAvatarLink() string { - u, err := url.Parse(setting.AppSubURL) - if err != nil { - log.Error("GetUserByEmail: %v", err) - return "" - } + once.Do(func() { + u, err := url.Parse(setting.AppSubURL) + if err != nil { + log.Error("Can not parse AppSubURL: %v", err) + return + } - u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") - return u.String() + u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") + defaultAvatarLink = u.String() + }) + return defaultAvatarLink } // HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/ diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index 943745c000f9..66f0d22efdfe 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 f0040fbbc1af..108d9b217afd 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 4b720c3f02a4..9423e5c5f695 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 000000000000..c6a5dc811c59 --- /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 c135bb9d3cc3..0ea2ce6886fd 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 7eac0f7bc52f..bce4afb0611b 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 59b7b6d5a834..aa62cc58e2d0 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 f2c0a322a085..dfa7f7b7524a 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 40b63aa80431..4e5cbfd55807 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 eeb33611ceb1..023f98c3eace 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 0af4640b7a4b..b05f34b64cca 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/models/webhook/hooktask.go b/models/webhook/hooktask.go index 1967ded298da..1d19ebd24e76 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -175,18 +175,13 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) { // CreateHookTask creates a new hook task, // it handles conversion from Payload to PayloadContent. func CreateHookTask(t *HookTask) error { - return createHookTask(db.GetEngine(db.DefaultContext), t) -} - -func createHookTask(e db.Engine, t *HookTask) error { data, err := t.Payloader.JSONPayload() if err != nil { return err } t.UUID = gouuid.New().String() t.PayloadContent = string(data) - _, err = e.Insert(t) - return err + return db.Insert(db.DefaultContext, t) } // UpdateHookTask updates information of hook task. @@ -195,6 +190,38 @@ func UpdateHookTask(t *HookTask) error { return err } +// ReplayHookTask copies a hook task to get re-delivered +func ReplayHookTask(hookID int64, uuid string) (*HookTask, error) { + var newTask *HookTask + + err := db.WithTx(func(ctx context.Context) error { + task := &HookTask{ + HookID: hookID, + UUID: uuid, + } + has, err := db.GetByBean(ctx, task) + if err != nil { + return err + } else if !has { + return ErrHookTaskNotExist{ + HookID: hookID, + UUID: uuid, + } + } + + newTask = &HookTask{ + UUID: gouuid.New().String(), + RepoID: task.RepoID, + HookID: task.HookID, + PayloadContent: task.PayloadContent, + EventType: task.EventType, + } + return db.Insert(ctx, newTask) + }) + + return newTask, err +} + // FindUndeliveredHookTasks represents find the undelivered hook tasks func FindUndeliveredHookTasks() ([]*HookTask, error) { tasks := make([]*HookTask, 0, 10) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index d01f548eed41..21c01d928999 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -41,6 +41,22 @@ func (err ErrWebhookNotExist) Error() string { return fmt.Sprintf("webhook does not exist [id: %d]", err.ID) } +// ErrHookTaskNotExist represents a "HookTaskNotExist" kind of error. +type ErrHookTaskNotExist struct { + HookID int64 + UUID string +} + +// IsErrWebhookNotExist checks if an error is a ErrWebhookNotExist. +func IsErrHookTaskNotExist(err error) bool { + _, ok := err.(ErrHookTaskNotExist) + return ok +} + +func (err ErrHookTaskNotExist) Error() string { + return fmt.Sprintf("hook task does not exist [hook: %d, uuid: %s]", err.HookID, err.UUID) +} + // HookContentType is the content type of a web hook type HookContentType int diff --git a/modules/context/org.go b/modules/context/org.go index eb81f6644c17..585a5fd762c6 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 f2b62a74bf7c..41a044c6d74e 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 18995f4ecd21..ed890ace4331 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/setting/mailer.go b/modules/setting/mailer.go index 1bcd63a914bd..d7713f3b80be 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -37,9 +37,10 @@ type Mailer struct { IsTLSEnabled bool // Sendmail sender - SendmailPath string - SendmailArgs []string - SendmailTimeout time.Duration + SendmailPath string + SendmailArgs []string + SendmailTimeout time.Duration + SendmailConvertCRLF bool } var ( @@ -71,8 +72,9 @@ func newMailService() { IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(), SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""), - SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"), - SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute), + SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"), + SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute), + SendmailConvertCRLF: sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true), } MailService.From = sec.Key("FROM").MustString(MailService.User) MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("") diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index 3b2c5e78391e..53e3fcf62da4 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 9164d5ffdced..3d60df5d682d 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 @@ -1831,12 +1831,13 @@ settings.webhook_deletion_desc = Removing a webhook deletes its settings and del settings.webhook_deletion_success = The webhook has been removed. settings.webhook.test_delivery = Test Delivery settings.webhook.test_delivery_desc = Test this webhook with a fake event. -settings.webhook.test_delivery_success = A fake event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history. settings.webhook.request = Request settings.webhook.response = Response settings.webhook.headers = Headers settings.webhook.payload = Content settings.webhook.body = Body +settings.webhook.replay.description = Replay this webhook. +settings.webhook.delivery.success = An event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history. settings.githooks_desc = "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations." settings.githook_edit_desc = If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook. settings.githook_name = Hook Name @@ -2261,9 +2262,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 +2897,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 d39125b0500e..cc7a63af337e 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 40fba5cd09a4..732f24b22c27 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/routers/web/repo/commit.go b/routers/web/repo/commit.go index e012bc24d287..304f76158cdb 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -254,7 +254,6 @@ func FileHistory(ctx *context.Context) { func Diff(ctx *context.Context) { ctx.Data["PageIsDiff"] = true ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true userName := ctx.Repo.Owner.Name diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 3b016b15fbd6..3b07c35cb015 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -764,7 +764,6 @@ func CompareDiff(ctx *context.Context) { ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsDiffCompare"] = true ctx.Data["RequireTribute"] = true - ctx.Data["RequireEasyMDE"] = true setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 12098ddc6911..1a7edb7bb40e 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -69,7 +69,6 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true canCommit := renderCommitRights(ctx) treePath := cleanUploadFileName(ctx.Repo.TreePath) @@ -200,7 +199,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["PageHasPosted"] = true ctx.Data["IsNewFile"] = isNewFile ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["TreePath"] = form.TreePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths @@ -544,7 +542,6 @@ func DeleteFilePost(ctx *context.Context) { func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true ctx.Data["RequireTribute"] = true - ctx.Data["RequireEasyMDE"] = true upload.AddUploadContext(ctx, "repo") canCommit := renderCommitRights(ctx) treePath := cleanUploadFileName(ctx.Repo.TreePath) @@ -580,7 +577,6 @@ func UploadFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UploadRepoFileForm) ctx.Data["PageIsUpload"] = true ctx.Data["RequireTribute"] = true - ctx.Data["RequireEasyMDE"] = true upload.AddUploadContext(ctx, "repo") canCommit := renderCommitRights(ctx) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ea16de3950b7..a28b37c58003 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -789,7 +789,6 @@ func NewIssue(ctx *context.Context) { ctx.Data["PageIsIssueList"] = true ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes title := ctx.FormString("title") @@ -962,7 +961,6 @@ func NewIssuePost(ctx *context.Context) { ctx.Data["PageIsIssueList"] = true ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -1147,7 +1145,6 @@ func ViewIssue(ctx *context.Context) { ctx.Data["RequireHighlightJS"] = true ctx.Data["RequireTribute"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index b40eb1ea170c..984954b704c1 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -738,7 +738,6 @@ func ViewPullFiles(ctx *context.Context) { setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) ctx.Data["RequireHighlightJS"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true if ctx.Data["Assignees"], err = models.GetRepoAssignees(ctx.Repo.Repository); err != nil { ctx.ServerError("GetAssignees", err) @@ -1098,7 +1097,6 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.Data["IsDiffCompare"] = true ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["RequireTribute"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireHighlightJS"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 8478cb44aef5..f2ab8b85f227 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -262,7 +262,6 @@ func LatestRelease(ctx *context.Context) { func NewRelease(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["PageIsReleaseList"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch if tagName := ctx.FormString("tag"); len(tagName) > 0 { @@ -296,7 +295,6 @@ func NewReleasePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewReleaseForm) ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["PageIsReleaseList"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true if ctx.HasError() { @@ -415,7 +413,6 @@ func EditRelease(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.edit_release") ctx.Data["PageIsReleaseList"] = true ctx.Data["PageIsEditRelease"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "release") @@ -454,7 +451,6 @@ func EditReleasePost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.edit_release") ctx.Data["PageIsReleaseList"] = true ctx.Data["PageIsEditRelease"] = true - ctx.Data["RequireEasyMDE"] = true ctx.Data["RequireTribute"] = true tagName := ctx.Params("*") diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index 47d84136718d..2ec2e8911f69 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -1189,11 +1189,33 @@ func TestWebhook(ctx *context.Context) { ctx.Flash.Error("PrepareWebhook: " + err.Error()) ctx.Status(500) } else { - ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success")) + ctx.Flash.Info(ctx.Tr("repo.settings.webhook.delivery.success")) ctx.Status(200) } } +// ReplayWebhook replays a webhook +func ReplayWebhook(ctx *context.Context) { + hookTaskUUID := ctx.Params(":uuid") + + orCtx, w := checkWebhook(ctx) + if ctx.Written() { + return + } + + if err := webhook_service.ReplayHookTask(w, hookTaskUUID); err != nil { + if webhook.IsErrHookTaskNotExist(err) { + ctx.NotFound("ReplayHookTask", nil) + } else { + ctx.ServerError("ReplayHookTask", err) + } + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.webhook.delivery.success")) + ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) +} + // DeleteWebhook delete a webhook func DeleteWebhook(ctx *context.Context) { if err := webhook.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 7bce7cf11cc1..d449800b8441 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -622,7 +622,6 @@ func WikiRaw(ctx *context.Context) { func NewWiki(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true - ctx.Data["RequireEasyMDE"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Data["title"] = "Home" @@ -639,7 +638,6 @@ func NewWikiPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewWikiForm) ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true - ctx.Data["RequireEasyMDE"] = true if ctx.HasError() { ctx.HTML(http.StatusOK, tplWikiNew) @@ -677,7 +675,6 @@ func NewWikiPost(ctx *context.Context) { func EditWiki(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWikiEdit"] = true - ctx.Data["RequireEasyMDE"] = true if !ctx.Repo.Repository.HasWiki() { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") @@ -697,7 +694,6 @@ func EditWikiPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewWikiForm) ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page") ctx.Data["PageIsWiki"] = true - ctx.Data["RequireEasyMDE"] = true if ctx.HasError() { ctx.HTML(http.StatusOK, tplWikiNew) diff --git a/routers/web/web.go b/routers/web/web.go index 2796904cee4a..8abdf7c61fb9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -189,7 +189,6 @@ func RegisterRoutes(m *web.Route) { ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true}) reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) - //bindIgnErr := binding.BindIgnErr bindIgnErr := web.Bind validation.AddBindingRules() @@ -436,7 +435,10 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", admin.DefaultOrSystemWebhooks) m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) - m.Get("/{id}", repo.WebHooksEdit) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -560,7 +562,10 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Get("/{id}", repo.WebHooksEdit) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -654,8 +659,11 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Get("/{id}", repo.WebHooksEdit) - m.Post("/{id}/test", repo.TestWebhook) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/test", repo.TestWebhook) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) diff --git a/services/forms/org.go b/services/forms/org.go index 7c8f17f95ee6..dec2dbfa6555 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/services/mailer/mailer.go b/services/mailer/mailer.go index eac2b15c3c2c..e5e6272f102e 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -290,13 +290,20 @@ func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error { return err } - _, err = msg.WriteTo(pipe) + if setting.MailService.SendmailConvertCRLF { + buf := &strings.Builder{} + _, err = msg.WriteTo(buf) + if err == nil { + _, err = strings.NewReplacer("\r\n", "\n").WriteString(pipe, buf.String()) + } + } else { + _, err = msg.WriteTo(pipe) + } // we MUST close the pipe or sendmail will hang waiting for more of the message // Also we should wait on our sendmail command even if something fails closeError = pipe.Close() waitError = cmd.Wait() - if err != nil { return err } else if closeError != nil { diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go index 56f2eb52b0bc..b94fce8443ae 100644 --- a/services/mailer/mailer_test.go +++ b/services/mailer/mailer_test.go @@ -14,14 +14,14 @@ import ( ) func TestGenerateMessageID(t *testing.T) { - var mailService = setting.Mailer{ + mailService := setting.Mailer{ From: "test@gitea.com", } setting.MailService = &mailService setting.Domain = "localhost" - date := time.Date(2000, 01, 02, 03, 04, 05, 06, time.UTC) + date := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) m := NewMessageFrom(nil, "display-name", "from-address", "subject", "body") m.Date = date gm := m.ToMessage() diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go index c8249dfdb2ba..7540037d92fa 100644 --- a/services/migrations/github_test.go +++ b/services/migrations/github_test.go @@ -17,7 +17,7 @@ import ( ) func TestGitHubDownloadRepo(t *testing.T) { - GithubLimitRateRemaining = 3 //Wait at 3 remaining since we could have 3 CI in // + GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in // downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo") err := downloader.RefreshRate() assert.NoError(t, err) @@ -364,7 +364,7 @@ func TestGitHubDownloadRepo(t *testing.T) { ReviewerID: 165205, ReviewerName: "lafriks", CommitID: "076160cf0b039f13e5eff19619932d181269414b", - CreatedAt: time.Date(2019, 11, 12, 21, 38, 00, 0, time.UTC), + CreatedAt: time.Date(2019, 11, 12, 21, 38, 0, 0, time.UTC), State: base.ReviewStateApproved, }, }, reviews) @@ -378,7 +378,7 @@ func TestGitHubDownloadRepo(t *testing.T) { ReviewerID: 81045, ReviewerName: "lunny", CommitID: "2be9101c543658591222acbee3eb799edfc3853d", - CreatedAt: time.Date(2020, 01, 04, 05, 33, 18, 0, time.UTC), + CreatedAt: time.Date(2020, 1, 4, 5, 33, 18, 0, time.UTC), State: base.ReviewStateApproved, Comments: []*base.ReviewComment{ { @@ -389,8 +389,8 @@ func TestGitHubDownloadRepo(t *testing.T) { Position: 3, CommitID: "2be9101c543658591222acbee3eb799edfc3853d", PosterID: 81045, - CreatedAt: time.Date(2020, 01, 04, 05, 33, 06, 0, time.UTC), - UpdatedAt: time.Date(2020, 01, 04, 05, 33, 18, 0, time.UTC), + CreatedAt: time.Date(2020, 1, 4, 5, 33, 6, 0, time.UTC), + UpdatedAt: time.Date(2020, 1, 4, 5, 33, 18, 0, time.UTC), }, }, }, @@ -400,7 +400,7 @@ func TestGitHubDownloadRepo(t *testing.T) { ReviewerID: 81045, ReviewerName: "lunny", CommitID: "2be9101c543658591222acbee3eb799edfc3853d", - CreatedAt: time.Date(2020, 01, 04, 06, 07, 06, 0, time.UTC), + CreatedAt: time.Date(2020, 1, 4, 6, 7, 6, 0, time.UTC), State: base.ReviewStateChangesRequested, Content: "Don't add more reviews", }, @@ -410,7 +410,7 @@ func TestGitHubDownloadRepo(t *testing.T) { ReviewerID: 81045, ReviewerName: "lunny", CommitID: "2be9101c543658591222acbee3eb799edfc3853d", - CreatedAt: time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC), + CreatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC), State: base.ReviewStateCommented, Comments: []*base.ReviewComment{ { @@ -421,8 +421,8 @@ func TestGitHubDownloadRepo(t *testing.T) { Position: 4, CommitID: "2be9101c543658591222acbee3eb799edfc3853d", PosterID: 81045, - CreatedAt: time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC), - UpdatedAt: time.Date(2020, 01, 04, 11, 21, 41, 0, time.UTC), + CreatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC), + UpdatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC), }, }, }, diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index f6dc7510cbed..6b77aa62efbf 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -156,7 +156,8 @@ func TestGitlabDownloadRepo(t *testing.T) { UserID: 1241334, UserName: "lafriks", Content: "open_mouth", - }}, + }, + }, Closed: timePtr(time.Date(2019, 11, 28, 8, 46, 23, 275000000, time.UTC)), }, { @@ -204,7 +205,8 @@ func TestGitlabDownloadRepo(t *testing.T) { UserID: 1241334, UserName: "lafriks", Content: "hearts", - }}, + }, + }, Closed: timePtr(time.Date(2019, 11, 28, 8, 45, 44, 959000000, time.UTC)), }, }, issues) @@ -310,13 +312,13 @@ func TestGitlabDownloadRepo(t *testing.T) { { ReviewerID: 4102996, ReviewerName: "zeripath", - CreatedAt: time.Date(2019, 11, 28, 16, 02, 8, 377000000, time.UTC), + CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), State: "APPROVED", }, { ReviewerID: 527793, ReviewerName: "axifive", - CreatedAt: time.Date(2019, 11, 28, 16, 02, 8, 377000000, time.UTC), + CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), State: "APPROVED", }, }, rvs) @@ -327,7 +329,7 @@ func TestGitlabDownloadRepo(t *testing.T) { { ReviewerID: 4575606, ReviewerName: "real6543", - CreatedAt: time.Date(2020, 04, 19, 19, 24, 21, 108000000, time.UTC), + CreatedAt: time.Date(2020, 4, 19, 19, 24, 21, 108000000, time.UTC), State: "APPROVED", }, }, rvs) diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go index 57eda59b928f..f9d74d3be3ec 100644 --- a/services/migrations/gogs_test.go +++ b/services/migrations/gogs_test.go @@ -99,8 +99,8 @@ func TestGogsDownloadRepo(t *testing.T) { Content: "test", Milestone: "", State: "open", - Created: time.Date(2019, 06, 11, 8, 16, 44, 0, time.UTC), - Updated: time.Date(2019, 10, 26, 11, 07, 2, 0, time.UTC), + Created: time.Date(2019, 6, 11, 8, 16, 44, 0, time.UTC), + Updated: time.Date(2019, 10, 26, 11, 7, 2, 0, time.UTC), Labels: []*base.Label{ { Name: "bug", @@ -121,8 +121,8 @@ func TestGogsDownloadRepo(t *testing.T) { PosterID: 5331, PosterName: "lunny", PosterEmail: "xiaolunwen@gmail.com", - Created: time.Date(2019, 06, 11, 8, 19, 50, 0, time.UTC), - Updated: time.Date(2019, 06, 11, 8, 19, 50, 0, time.UTC), + Created: time.Date(2019, 6, 11, 8, 19, 50, 0, time.UTC), + Updated: time.Date(2019, 6, 11, 8, 19, 50, 0, time.UTC), Content: "1111", }, { diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 9356f4ee11f1..f284a20c306a 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -229,3 +229,15 @@ func prepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventT } return nil } + +// ReplayHookTask replays a webhook task +func ReplayHookTask(w *webhook_model.Webhook, uuid string) error { + t, err := webhook_model.ReplayHookTask(w.ID, uuid) + if err != nil { + return err + } + + go hookQueue.Add(t.RepoID) + + return nil +} diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 4710498b20a0..2cd3fc826c16 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 2b641cc9de73..122b3b46b4e9 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -12,14 +12,6 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} -{{if .RequireEasyMDE}} - - - - -{{end}} {{if .RequireU2F}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 8bc6ae689d32..499c113abf06 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -59,9 +59,6 @@ -{{if .RequireEasyMDE}} - -{{end}}