[GITEA] 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 <forgejo@gergo.csillger.hu>
(cherry picked from commit 4b74439922)
(cherry picked from commit 337cf62c10)
(cherry picked from commit b6786fdb32)
This commit is contained in:
Gergely Nagy 2023-12-20 21:44:55 +01:00 committed by Earl Warren
parent 65dbc4303b
commit a5d2829a10
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
10 changed files with 165 additions and 10 deletions

View file

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/forgejo/semver" "code.gitea.io/gitea/models/forgejo/semver"
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20" 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/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -43,6 +44,8 @@ var migrations = []*Migration{
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
// v2 -> v3 // v2 -> v3
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), 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. // GetCurrentDBVersion returns the current Forgejo database version.

View file

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

View file

@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool {
return p.AccessMode >= perm_model.AccessModeAdmin 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 // HasAccess returns true if the current user has at least read access to any unit of this repository
func (p *Permission) HasAccess() bool { func (p *Permission) HasAccess() bool {
if p.UnitsMode == nil { 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 { if err := repo.LoadOwner(ctx); err != nil {
return perm, err return perm, err
} }
if !repo.Owner.IsOrganization() { 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 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 !found && !repo.IsPrivate && !user.IsRestricted {
if _, ok := perm.UnitsMode[u.Type]; !ok { if _, ok := perm.UnitsMode[u.Type]; !ok {
perm.UnitsMode[u.Type] = perm_model.AccessModeRead perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead)
} }
} }
} }

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -39,6 +40,35 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
return util.ErrNotExist 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 // RepoUnit describes all units of a repository
type RepoUnit struct { //revive:disable-line:exported type RepoUnit struct { //revive:disable-line:exported
ID int64 ID int64
@ -46,6 +76,7 @@ type RepoUnit struct { //revive:disable-line:exported
Type unit.Type `xorm:"INDEX(s)"` Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"` Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"`
} }
func init() { func init() {

View file

@ -6,6 +6,8 @@ package repo
import ( import (
"testing" "testing"
"code.gitea.io/gitea/models/perm"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) {
cfg.DisableWorkflow("test3.yaml") cfg.DisableWorkflow("test3.yaml")
assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) 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)
}

View file

@ -2039,6 +2039,7 @@ settings.branches.update_default_branch = Update Default Branch
settings.branches.add_new_rule = Add New Rule settings.branches.add_new_rule = Add New Rule
settings.advanced_settings = Advanced Settings settings.advanced_settings = Advanced Settings
settings.wiki_desc = Enable Repository Wiki 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_internal_wiki = Use Built-In Wiki
settings.use_external_wiki = Use External Wiki settings.use_external_wiki = Use External Wiki
settings.external_wiki_url = External Wiki URL settings.external_wiki_url = External Wiki URL

View file

@ -473,10 +473,17 @@ func SettingsPost(ctx *context.Context) {
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { } 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{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: unit_model.TypeWiki, Type: unit_model.TypeWiki,
Config: new(repo_model.UnitConfig), Config: new(repo_model.UnitConfig),
DefaultPermissions: wikiPermissions,
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
} else { } else {

View file

@ -140,6 +140,7 @@ type RepoSettingForm struct {
// Advanced settings // Advanced settings
EnableCode bool EnableCode bool
EnableWiki bool EnableWiki bool
GloballyWriteableWiki bool
EnableExternalWiki bool EnableExternalWiki bool
ExternalWikiURL string ExternalWikiURL string
EnableIssues bool EnableIssues bool

View file

@ -318,6 +318,16 @@
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label> <label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
</div> </div>
</div> </div>
{{if (not .Repository.IsPrivate)}}
<div class="field {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}">
<div class="field">
<div class="ui checkbox">
<input name="globally_writeable_wiki" type="checkbox" {{if .Permission.IsGloballyWriteable $.UnitTypeWiki}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.wiki_globally_editable"}}</label>
</div>
</div>
</div>
{{end}}
<div class="field"> <div class="field">
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context $.UnitTypeExternalWiki}}checked{{end}}> <input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context $.UnitTypeExternalWiki}}checked{{end}}>

View file

@ -4,13 +4,18 @@
package integration package integration
import ( import (
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" 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" api "code.gitea.io/gitea/modules/structs"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -209,6 +214,53 @@ func TestAPIEditWikiPage(t *testing.T) {
MakeRequest(t, req, http.StatusOK) 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_service.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) { func TestAPIListPageRevisions(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
username := "user2" username := "user2"