From 4b744399229f255eb124c22e3969715046043209 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Wed, 20 Dec 2023 21:44:55 +0100 Subject: [PATCH] Optionally allow anyone to edit Wikis This is largely based on gitea#6312 by @ashimokawa, with updates and fixes by myself, and incorporates the review feedback given in that pull request, and more. What this patch does is add a new "default_permissions" column to the `repo_units` table (defaulting to read permission), adjusts the permission checking code to take this into consideration, and then exposes a setting that lets a repo administrator enable any user on a Forgejo instance to edit the repo's wiki (effectively giving the wiki unit of the repo "write" permissions by default). By default, wikis will remain restricted to collaborators, but with the new setting exposed, they can be turned into globally editable wikis. Fixes Codeberg/Community#28. Signed-off-by: Gergely Nagy --- models/forgejo_migrations/migrate.go | 3 ++ models/forgejo_migrations/v1_22/v4.go | 17 +++++++++ models/perm/access/repo_permission.go | 28 +++++++++++++-- models/repo/repo_unit.go | 41 ++++++++++++++++++--- models/repo/repo_unit_test.go | 9 +++++ options/locale/locale_en-US.ini | 1 + routers/web/repo/setting/setting.go | 13 +++++-- services/forms/repo_form.go | 1 + templates/repo/settings/options.tmpl | 10 ++++++ tests/integration/api_wiki_test.go | 51 +++++++++++++++++++++++++++ 10 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 models/forgejo_migrations/v1_22/v4.go diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 58f158bd17..8ac2fb63f2 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/forgejo/semver" forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20" + forgejo_v1_22 "code.gitea.io/gitea/models/forgejo_migrations/v1_22" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -43,6 +44,8 @@ var migrations = []*Migration{ NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), // v2 -> v3 NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), + // v3 -> v4 + NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v1_22/v4.go b/models/forgejo_migrations/v1_22/v4.go new file mode 100644 index 0000000000..f1195f5f66 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v4.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +func AddDefaultPermissionsToRepoUnit(x *xorm.Engine) error { + type RepoUnit struct { + ID int64 + DefaultPermissions int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync(&RepoUnit{}) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 395ecdf1a5..0b66e62d7d 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool { return p.AccessMode >= perm_model.AccessModeAdmin } +// IsGloballyWriteable returns true if the unit is writeable by all users of the instance. +func (p *Permission) IsGloballyWriteable(unitType unit.Type) bool { + for _, u := range p.Units { + if u.Type == unitType { + return u.DefaultPermissions == repo_model.UnitAccessModeWrite + } + } + return false +} + // HasAccess returns true if the current user has at least read access to any unit of this repository func (p *Permission) HasAccess() bool { if p.UnitsMode == nil { @@ -198,7 +208,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use if err := repo.LoadOwner(ctx); err != nil { return perm, err } + if !repo.Owner.IsOrganization() { + // for a public repo, different repo units may have different default + // permissions for non-restricted users. + if !repo.IsPrivate && !user.IsRestricted && len(repo.Units) > 0 { + perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + for _, u := range repo.Units { + if _, ok := perm.UnitsMode[u.Type]; !ok { + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm.AccessMode) + } + } + } + return perm, nil } @@ -239,10 +261,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. + // for a public repo on an organization, a non-restricted user should + // have the same permission on non-team defined units as the default + // permissions for the repo unit. if !found && !repo.IsPrivate && !user.IsRestricted { if _, ok := perm.UnitsMode[u.Type]; !ok { - perm.UnitsMode[u.Type] = perm_model.AccessModeRead + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead) } } } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 89f28b8fca..3df5236ea7 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" @@ -39,13 +40,43 @@ func (err ErrUnitTypeNotExist) Unwrap() error { return util.ErrNotExist } +// RepoUnitAccessMode specifies the users access mode to a repo unit +type UnitAccessMode int + +const ( + // UnitAccessModeUnset - no unit mode set + UnitAccessModeUnset UnitAccessMode = iota // 0 + // UnitAccessModeNone no access + UnitAccessModeNone // 1 + // UnitAccessModeRead read access + UnitAccessModeRead // 2 + // UnitAccessModeWrite write access + UnitAccessModeWrite // 3 +) + +func (mode UnitAccessMode) ToAccessMode(modeIfUnset perm.AccessMode) perm.AccessMode { + switch mode { + case UnitAccessModeUnset: + return modeIfUnset + case UnitAccessModeNone: + return perm.AccessModeNone + case UnitAccessModeRead: + return perm.AccessModeRead + case UnitAccessModeWrite: + return perm.AccessModeWrite + default: + return perm.AccessModeNone + } +} + // RepoUnit describes all units of a repository type RepoUnit struct { //revive:disable-line:exported - ID int64 - RepoID int64 `xorm:"INDEX(s)"` - Type unit.Type `xorm:"INDEX(s)"` - Config convert.Conversion `xorm:"TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type unit.Type `xorm:"INDEX(s)"` + Config convert.Conversion `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"` } func init() { diff --git a/models/repo/repo_unit_test.go b/models/repo/repo_unit_test.go index a760594013..27a34fd0eb 100644 --- a/models/repo/repo_unit_test.go +++ b/models/repo/repo_unit_test.go @@ -6,6 +6,8 @@ package repo import ( "testing" + "code.gitea.io/gitea/models/perm" + "github.com/stretchr/testify/assert" ) @@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) { cfg.DisableWorkflow("test3.yaml") assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) } + +func TestRepoUnitAccessMode(t *testing.T) { + assert.Equal(t, UnitAccessModeNone.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeNone) + assert.Equal(t, UnitAccessModeRead.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeRead) + assert.Equal(t, UnitAccessModeWrite.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeWrite) + assert.Equal(t, UnitAccessModeUnset.ToAccessMode(perm.AccessModeRead), perm.AccessModeRead) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a5c82bfcf6..40e1a3ecbe 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2036,6 +2036,7 @@ settings.branches.update_default_branch = Update Default Branch settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki +settings.wiki_globally_editable = Allow anyone to edit the Wiki settings.use_internal_wiki = Use Built-In Wiki settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 69cbcdbf8d..e72c89be4c 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -473,10 +473,17 @@ func SettingsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + var wikiPermissions repo_model.UnitAccessMode + if form.GloballyWriteableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: wikiPermissions, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) } else { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ed4d82e1e5..c1dfce0a87 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -140,6 +140,7 @@ type RepoSettingForm struct { // Advanced settings EnableCode bool EnableWiki bool + GloballyWriteableWiki bool EnableExternalWiki bool ExternalWikiURL string EnableIssues bool diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 80aec2b252..5c2da5ebbd 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -318,6 +318,16 @@ + {{if (not .Repository.IsPrivate)}} +
+
+
+ + +
+
+
+ {{end}}
diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go index 05d90fc4e3..3b5469a1a5 100644 --- a/tests/integration/api_wiki_test.go +++ b/tests/integration/api_wiki_test.go @@ -4,12 +4,16 @@ package integration import ( + "context" "encoding/base64" "fmt" "net/http" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -209,6 +213,53 @@ func TestAPIEditWikiPage(t *testing.T) { MakeRequest(t, req, http.StatusOK) } +func TestAPIEditOtherWikiPage(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // (drive-by-user) user, session, and token for a drive-by wiki editor + username := "drive-by-user" + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": username, + "email": "drive-by@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + MakeRequest(t, req, http.StatusSeeOther) + session := loginUserWithPassword(t, username, "examplePassword!1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // (user2) user for the user whose wiki we're going to edit (as drive-by-user) + otherUsername := "user2" + + // Creating a new Wiki page on user2's repo as user1 fails + testCreateWiki := func(expectedStatusCode int) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new?token=%s", otherUsername, "repo1", token) + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{ + Title: "Globally Edited Page", + ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")), + Message: "", + }) + session.MakeRequest(t, req, expectedStatusCode) + } + testCreateWiki(http.StatusForbidden) + + // Update the repo settings for user2's repo to enable globally writeable wiki + ctx := context.Background() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + var units []repo_model.RepoUnit + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: repo_model.UnitAccessModeWrite, + }) + err := repo_model.UpdateRepositoryUnits(ctx, repo, units, nil) + assert.NoError(t, err) + + // Creating a new Wiki page on user2's repo works now + testCreateWiki(http.StatusCreated) +} + func TestAPIListPageRevisions(t *testing.T) { defer tests.PrepareTestEnv(t)() username := "user2"