[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 commit4b74439922
) (cherry picked from commit337cf62c10
) (cherry picked from commitb6786fdb32
) (cherry picked from commita5d2829a10
) [GITEA] Optionally allow anyone to edit Wikis (squash) AddTokenAuth (cherry picked from commitfed50cf72e
) (cherry picked from commit42c55e494e
) (cherry picked from commite3463bda47
)
This commit is contained in:
parent
f3f888ed13
commit
5eeccecafc
|
@ -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.
|
||||||
|
|
|
@ -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{})
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,13 +40,43 @@ 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
|
||||||
RepoID int64 `xorm:"INDEX(s)"`
|
RepoID int64 `xorm:"INDEX(s)"`
|
||||||
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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -132,6 +132,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
|
||||||
|
|
|
@ -335,6 +335,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}}>
|
||||||
|
|
|
@ -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", otherUsername, "repo1")
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
|
||||||
|
Title: "Globally Edited Page",
|
||||||
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
|
||||||
|
Message: "",
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
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"
|
||||||
|
|
Loading…
Reference in New Issue