aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-04-17 23:58:37 +0800
committerGitHub <noreply@github.com>2024-04-17 15:58:37 +0000
commit3feba9f1f44156c256a30d25ad1c25f751819c94 (patch)
tree77d9575edc49e625b54f5358d67dba0764b1a553
parentbafb80f80d5505b03e5994d1ea6e2dab10052fe1 (diff)
downloadgitea-3feba9f1f44156c256a30d25ad1c25f751819c94.tar.gz
gitea-3feba9f1f44156c256a30d25ad1c25f751819c94.zip
Allow everyone to read or write a wiki by a repo unit setting (#30495)
Replace #6312 Help #5833 Wiki solution for #639
-rw-r--r--models/issues/pull_list.go6
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v1_11/v111.go2
-rw-r--r--models/migrations/v1_23/v297.go17
-rw-r--r--models/organization/team.go4
-rw-r--r--models/perm/access/access.go8
-rw-r--r--models/perm/access/repo_permission.go139
-rw-r--r--models/perm/access/repo_permission_test.go98
-rw-r--r--models/perm/access_mode.go39
-rw-r--r--models/perm/access_mode_test.go22
-rw-r--r--models/repo/repo_unit.go12
-rw-r--r--models/unit/unit.go11
-rw-r--r--modules/templates/helper.go12
-rw-r--r--options/locale/locale_en-US.ini2
-rw-r--r--routers/api/v1/api.go6
-rw-r--r--routers/private/hook_pre_receive.go6
-rw-r--r--routers/web/repo/setting/setting.go8
-rw-r--r--routers/web/repo/view.go3
-rw-r--r--services/convert/convert.go2
-rw-r--r--services/convert/repository.go11
-rw-r--r--services/convert/user.go4
-rw-r--r--services/forms/repo_form.go1
-rw-r--r--templates/repo/settings/options.tmpl28
-rw-r--r--tests/integration/api_team_test.go4
24 files changed, 319 insertions, 128 deletions
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index de3eceed37..b5557cad06 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -62,11 +62,13 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission,
return true
}
- if len(p.Units) < 1 {
+ // the code below depends on units to get the repository ID, not ideal but just keep it for now
+ firstUnitRepoID := p.GetFirstUnitRepoID()
+ if firstUnitRepoID == 0 {
return false
}
- prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch)
+ prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
if err != nil {
return false
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 5326d48f90..cb3a64f48c 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -582,6 +582,8 @@ var migrations = []Migration{
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary),
// v296 -> v297
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2),
+ // v297 -> v298
+ NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go
index d757acb7d2..1722792a38 100644
--- a/models/migrations/v1_11/v111.go
+++ b/models/migrations/v1_11/v111.go
@@ -336,7 +336,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
if err != nil {
return false, err
}
- if perm.UnitsMode == nil {
+ if len(perm.UnitsMode) == 0 {
for _, u := range perm.Units {
if u.Type == UnitTypeCode {
return AccessModeWrite <= perm.AccessMode, nil
diff --git a/models/migrations/v1_23/v297.go b/models/migrations/v1_23/v297.go
new file mode 100644
index 0000000000..e79f04cf9c
--- /dev/null
+++ b/models/migrations/v1_23/v297.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+ "code.gitea.io/gitea/models/perm"
+
+ "xorm.io/xorm"
+)
+
+func AddRepoUnitEveryoneAccessMode(x *xorm.Engine) error {
+ type RepoUnit struct { //revive:disable-line:exported
+ EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
+ }
+ return x.Sync(&RepoUnit{})
+}
diff --git a/models/organization/team.go b/models/organization/team.go
index 501a43d3a1..e4e83fedee 100644
--- a/models/organization/team.go
+++ b/models/organization/team.go
@@ -130,11 +130,11 @@ 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()
+ m[u.NameKey] = t.AccessMode.ToString()
}
} else {
for _, u := range t.Units {
- m[u.Unit().NameKey] = u.AccessMode.String()
+ m[u.Unit().NameKey] = u.AccessMode.ToString()
}
}
return m
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index b422a08614..6a0a901f71 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -63,13 +63,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
}
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode {
- max := perm.AccessModeNone
+ maxMode := perm.AccessModeNone
for _, mode := range modes {
- if mode > max {
- max = mode
- }
+ maxMode = max(maxMode, mode)
}
- return max
+ return maxMode
}
type userAccess struct {
diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index e4e7579e62..9cce95b776 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -6,6 +6,7 @@ package access
import (
"context"
"fmt"
+ "slices"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
@@ -14,13 +15,15 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
)
// Permission contains all the permissions related variables to a repository for a user
type Permission struct {
AccessMode perm_model.AccessMode
- Units []*repo_model.RepoUnit
- UnitsMode map[unit.Type]perm_model.AccessMode
+
+ units []*repo_model.RepoUnit
+ unitsMode map[unit.Type]perm_model.AccessMode
}
// IsOwner returns true if current user is the owner of repository.
@@ -33,25 +36,44 @@ func (p *Permission) IsAdmin() bool {
return p.AccessMode >= perm_model.AccessModeAdmin
}
-// 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 might have at least read access to any unit of this repository
func (p *Permission) HasAccess() bool {
- if p.UnitsMode == nil {
- return p.AccessMode >= perm_model.AccessModeRead
+ return len(p.unitsMode) > 0 || p.AccessMode >= perm_model.AccessModeRead
+}
+
+// HasUnits returns true if the permission contains attached units
+func (p *Permission) HasUnits() bool {
+ return len(p.units) > 0
+}
+
+// GetFirstUnitRepoID returns the repo ID of the first unit, it is a fragile design and should NOT be used anymore
+// deprecated
+func (p *Permission) GetFirstUnitRepoID() int64 {
+ if len(p.units) > 0 {
+ return p.units[0].RepoID
}
- return len(p.UnitsMode) > 0
+ return 0
}
-// UnitAccessMode returns current user accessmode to the specify unit of the repository
+// UnitAccessMode returns current user access mode to the specify unit of the repository
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
- if p.UnitsMode == nil {
- for _, u := range p.Units {
- if u.Type == unitType {
- return p.AccessMode
- }
+ if p.unitsMode != nil {
+ // if the units map contains the access mode, use it, but admin/owner mode could override it
+ if m, ok := p.unitsMode[unitType]; ok {
+ return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m)
}
- return perm_model.AccessModeNone
}
- return p.UnitsMode[unitType]
+ // if the units map does not contain the access mode, return the default access mode if the unit exists
+ hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
+ return util.Iif(hasUnit, p.AccessMode, perm_model.AccessModeNone)
+}
+
+func (p *Permission) SetUnitsWithDefaultAccessMode(units []*repo_model.RepoUnit, mode perm_model.AccessMode) {
+ p.units = units
+ p.unitsMode = make(map[unit.Type]perm_model.AccessMode)
+ for _, u := range p.units {
+ p.unitsMode[u.Type] = mode
+ }
}
// CanAccess returns true if user has mode access to the unit of the repository
@@ -103,8 +125,8 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
}
func (p *Permission) ReadableUnitTypes() []unit.Type {
- types := make([]unit.Type, 0, len(p.Units))
- for _, u := range p.Units {
+ types := make([]unit.Type, 0, len(p.units))
+ for _, u := range p.units {
if p.CanRead(u.Type) {
types = append(types, u.Type)
}
@@ -114,21 +136,21 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
func (p *Permission) LogString() string {
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ "
- args := []any{p.AccessMode.String(), len(p.Units), len(p.UnitsMode)}
+ args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
- for i, unit := range p.Units {
+ for i, u := range p.units {
config := ""
- if unit.Config != nil {
- configBytes, err := unit.Config.ToDB()
+ if u.Config != nil {
+ configBytes, err := u.Config.ToDB()
config = string(configBytes)
if err != nil {
config = err.Error()
}
}
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s"
- args = append(args, i, unit.ID, unit.RepoID, unit.Type.LogString(), config)
+ args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
}
- for key, value := range p.UnitsMode {
+ for key, value := range p.unitsMode {
format += "\nUnitMode[%-v]: %-v"
args = append(args, key.LogString(), value.LogString())
}
@@ -136,23 +158,34 @@ func (p *Permission) LogString() string {
return fmt.Sprintf(format, args...)
}
-// GetUserRepoPermission returns the user permissions to the repository
-func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) {
- var perm Permission
- if log.IsTrace() {
- defer func() {
- if user == nil {
- log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v",
- repo,
- perm)
- return
+func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
+ if user != nil && user.ID > 0 {
+ for _, u := range perm.units {
+ if perm.unitsMode == nil {
+ perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
}
- log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v",
- user,
- repo,
- perm)
- }()
+ if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.unitsMode[u.Type] {
+ perm.unitsMode[u.Type] = u.EveryoneAccessMode
+ }
+ }
+ }
+}
+
+// GetUserRepoPermission returns the user permissions to the repository
+func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
+ defer func() {
+ if err == nil {
+ applyEveryoneRepoPermission(user, &perm)
+ }
+ if log.IsTrace() {
+ log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
+ }
+ }()
+
+ if err = repo.LoadUnits(ctx); err != nil {
+ return perm, err
}
+ perm.units = repo.Units
// anonymous user visit private repo.
// TODO: anonymous user visit public unit of private repo???
@@ -162,7 +195,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
}
var isCollaborator bool
- var err error
if user != nil {
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
if err != nil {
@@ -170,7 +202,7 @@ 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
}
@@ -181,12 +213,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, nil
}
- if err := repo.LoadUnits(ctx); err != nil {
- return perm, err
- }
-
- perm.Units = repo.Units
-
// anonymous visit public repo
if user == nil {
perm.AccessMode = perm_model.AccessModeRead
@@ -205,19 +231,16 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, err
}
- if err := repo.LoadOwner(ctx); err != nil {
- return perm, err
- }
if !repo.Owner.IsOrganization() {
return perm, nil
}
- perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode)
+ perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
// Collaborators on organization
if isCollaborator {
for _, u := range repo.Units {
- perm.UnitsMode[u.Type] = perm.AccessMode
+ perm.unitsMode[u.Type] = perm.AccessMode
}
}
@@ -231,7 +254,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
for _, team := range teams {
if team.AccessMode >= perm_model.AccessModeAdmin {
perm.AccessMode = perm_model.AccessModeOwner
- perm.UnitsMode = nil
+ perm.unitsMode = nil
return perm, nil
}
}
@@ -240,25 +263,25 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
var found bool
for _, team := range teams {
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
- perm.UnitsMode[u.Type] = max(perm.UnitsMode[u.Type], teamMode)
+ perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
found = true
}
}
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
if !found && !repo.IsPrivate && !user.IsRestricted {
- if _, ok := perm.UnitsMode[u.Type]; !ok {
- perm.UnitsMode[u.Type] = perm_model.AccessModeRead
+ if _, ok := perm.unitsMode[u.Type]; !ok {
+ perm.unitsMode[u.Type] = perm_model.AccessModeRead
}
}
}
// remove no permission units
- perm.Units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
- for t := range perm.UnitsMode {
+ perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
+ for t := range perm.unitsMode {
for _, u := range repo.Units {
if u.Type == t {
- perm.Units = append(perm.Units, u)
+ perm.units = append(perm.units, u)
}
}
}
@@ -340,7 +363,7 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
if user.IsOrganization() {
- return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
+ return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
}
perm, err := GetUserRepoPermission(ctx, repo, user)
if err != nil {
diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go
new file mode 100644
index 0000000000..aaa53bb24f
--- /dev/null
+++ b/models/perm/access/repo_permission_test.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package access
+
+import (
+ "testing"
+
+ perm_model "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestApplyEveryoneRepoPermission(t *testing.T) {
+ perm := Permission{
+ AccessMode: perm_model.AccessModeNone,
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeNone},
+ },
+ }
+ applyEveryoneRepoPermission(nil, &perm)
+ assert.False(t, perm.CanRead(unit.TypeWiki))
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeNone,
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
+ },
+ }
+ applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
+ assert.True(t, perm.CanRead(unit.TypeWiki))
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeWrite,
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
+ },
+ }
+ applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
+ assert.True(t, perm.CanRead(unit.TypeWiki))
+ assert.False(t, perm.CanWrite(unit.TypeWiki)) // because there is no unit mode, so the everyone-mode is used as the unit's access mode
+
+ perm = Permission{
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
+ },
+ unitsMode: map[unit.Type]perm_model.AccessMode{
+ unit.TypeWiki: perm_model.AccessModeWrite,
+ },
+ }
+ applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
+ assert.True(t, perm.CanWrite(unit.TypeWiki))
+}
+
+func TestUnitAccessMode(t *testing.T) {
+ perm := Permission{
+ AccessMode: perm_model.AccessModeNone,
+ }
+ assert.Equal(t, perm_model.AccessModeNone, perm.UnitAccessMode(unit.TypeWiki), "no unit, no map, use AccessMode")
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeRead,
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki},
+ },
+ }
+ assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "only unit, no map, use AccessMode")
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeAdmin,
+ unitsMode: map[unit.Type]perm_model.AccessMode{
+ unit.TypeWiki: perm_model.AccessModeRead,
+ },
+ }
+ assert.Equal(t, perm_model.AccessModeAdmin, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, admin overrides map")
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeNone,
+ unitsMode: map[unit.Type]perm_model.AccessMode{
+ unit.TypeWiki: perm_model.AccessModeRead,
+ },
+ }
+ assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, use map")
+
+ perm = Permission{
+ AccessMode: perm_model.AccessModeNone,
+ units: []*repo_model.RepoUnit{
+ {Type: unit.TypeWiki},
+ },
+ unitsMode: map[unit.Type]perm_model.AccessMode{
+ unit.TypeWiki: perm_model.AccessModeRead,
+ },
+ }
+ assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
+}
diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go
index a37bc1f0e1..0364191e2e 100644
--- a/models/perm/access_mode.go
+++ b/models/perm/access_mode.go
@@ -5,25 +5,25 @@ package perm
import (
"fmt"
+ "slices"
+
+ "code.gitea.io/gitea/modules/util"
)
// AccessMode specifies the users access mode
type AccessMode int
const (
- // AccessModeNone no access
- AccessModeNone AccessMode = iota // 0
- // AccessModeRead read access
- AccessModeRead // 1
- // AccessModeWrite write access
- AccessModeWrite // 2
- // AccessModeAdmin admin access
- AccessModeAdmin // 3
- // AccessModeOwner owner access
- AccessModeOwner // 4
+ AccessModeNone AccessMode = iota // 0: no access
+
+ AccessModeRead // 1: read access
+ AccessModeWrite // 2: write access
+ AccessModeAdmin // 3: admin access
+ AccessModeOwner // 4: owner access
)
-func (mode AccessMode) String() string {
+// ToString returns the string representation of the access mode, do not make it a Stringer, otherwise it's difficult to render in templates
+func (mode AccessMode) ToString() string {
switch mode {
case AccessModeRead:
return "read"
@@ -39,19 +39,24 @@ func (mode AccessMode) String() string {
}
func (mode AccessMode) LogString() string {
- return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.String())
+ return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.ToString())
}
// ParseAccessMode returns corresponding access mode to given permission string.
-func ParseAccessMode(permission string) AccessMode {
+func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode {
+ m := AccessModeNone
switch permission {
case "read":
- return AccessModeRead
+ m = AccessModeRead
case "write":
- return AccessModeWrite
+ m = AccessModeWrite
case "admin":
- return AccessModeAdmin
+ m = AccessModeAdmin
default:
- return AccessModeNone
+ // the "owner" access is not really used for user input, it's mainly for checking access level in code, so don't parse it
+ }
+ if len(allowed) == 0 {
+ return m
}
+ return util.Iif(slices.Contains(allowed, m), m, AccessModeNone)
}
diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go
new file mode 100644
index 0000000000..982fceee5a
--- /dev/null
+++ b/models/perm/access_mode_test.go
@@ -0,0 +1,22 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package perm
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAccessMode(t *testing.T) {
+ names := []string{"none", "read", "write", "admin"}
+ for i, name := range names {
+ m := ParseAccessMode(name)
+ assert.Equal(t, AccessMode(i), m)
+ }
+ assert.Equal(t, AccessMode(4), AccessModeOwner)
+ assert.Equal(t, "owner", AccessModeOwner.ToString())
+ assert.Equal(t, AccessModeNone, ParseAccessMode("owner"))
+ assert.Equal(t, AccessModeNone, ParseAccessMode("invalid"))
+}
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 5a841f4d31..fd5baa9488 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"
@@ -41,11 +42,12 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
// 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"`
+ EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}
func init() {
diff --git a/models/unit/unit.go b/models/unit/unit.go
index b216712d37..a78a2f1e47 100644
--- a/models/unit/unit.go
+++ b/models/unit/unit.go
@@ -191,16 +191,13 @@ type Unit struct {
NameKey string
URI string
DescKey string
- Idx int
+ Priority int
MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read.
}
// IsLessThan compares order of two units
func (u Unit) IsLessThan(unit Unit) bool {
- if (u.Type == TypeExternalTracker || u.Type == TypeExternalWiki) && unit.Type != TypeExternalTracker && unit.Type != TypeExternalWiki {
- return false
- }
- return u.Idx < unit.Idx
+ return u.Priority < unit.Priority
}
// MaxPerm returns the max perms of this unit
@@ -236,7 +233,7 @@ var (
"repo.ext_issues",
"/issues",
"repo.ext_issues.desc",
- 1,
+ 101,
perm.AccessModeRead,
}
@@ -272,7 +269,7 @@ var (
"repo.ext_wiki",
"/wiki",
"repo.ext_wiki.desc",
- 4,
+ 102,
perm.AccessModeRead,
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 5d2fa79bc5..360b48c594 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -34,6 +34,7 @@ func NewFuncMap() template.FuncMap {
// -----------------------------------------------------------------
// html/template related functions
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+ "Iif": Iif,
"Eval": Eval,
"SafeHTML": SafeHTML,
"HTMLFormat": HTMLFormat,
@@ -238,6 +239,17 @@ func DotEscape(raw string) string {
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
}
+// Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
+// and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
+func Iif(condition bool, vals ...any) any {
+ if condition {
+ return vals[0]
+ } else if len(vals) > 1 {
+ return vals[1]
+ }
+ return nil
+}
+
// Eval the expression and return the result, see the comment of eval.Expr for details.
// To use this helper function in templates, pass each token as a separate parameter.
//
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index ed274197c7..a7c1d91791 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -885,6 +885,7 @@ repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only
permissions_access_all = All (public, private, and limited)
select_permissions = Select permissions
+permission_not_set = Not set
permission_no_access = No Access
permission_read = Read
permission_write = Read and Write
@@ -2096,6 +2097,7 @@ settings.advanced_settings = Advanced Settings
settings.wiki_desc = Enable Repository Wiki
settings.use_internal_wiki = Use Built-In Wiki
settings.default_wiki_branch_name = Default Wiki Branch Name
+settings.default_wiki_everyone_access = Default Access Permission for signed-in users:
settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
settings.use_external_wiki = Use External Wiki
settings.external_wiki_url = External Wiki URL
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 1fc7682966..f60c5f21db 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -209,11 +209,7 @@ func repoAssignment() func(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadUnits", err)
return
}
- ctx.Repo.Permission.Units = ctx.Repo.Repository.Units
- ctx.Repo.Permission.UnitsMode = make(map[unit.Type]perm.AccessMode)
- for _, u := range ctx.Repo.Repository.Units {
- ctx.Repo.Permission.UnitsMode[u.Type] = ctx.Repo.Permission.AccessMode
- }
+ ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 32ec3003e2..4e59237ed3 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -481,11 +481,7 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
})
return false
}
- ctx.userPerm.Units = ctx.Repo.Repository.Units
- ctx.userPerm.UnitsMode = make(map[unit.Type]perm_model.AccessMode)
- for _, u := range ctx.Repo.Repository.Units {
- ctx.userPerm.UnitsMode[u.Type] = ctx.userPerm.AccessMode
- }
+ ctx.userPerm.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.userPerm.AccessMode)
} else {
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 00a5282f34..b55e259e4b 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -16,6 +16,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -476,9 +477,10 @@ func SettingsPost(ctx *context.Context) {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
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),
+ EveryoneAccessMode: perm.ParseAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
} else {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index de35c6b3a2..9c1f4faa5f 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -684,7 +684,7 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i
}
func checkHomeCodeViewable(ctx *context.Context) {
- if len(ctx.Repo.Units) > 0 {
+ if ctx.Repo.HasUnits() {
if ctx.Repo.Repository.IsBeingCreated() {
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
if err != nil {
@@ -723,6 +723,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
var firstUnit *unit_model.Unit
for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() {
if repoUnitType == unit_model.TypeCode {
+ // we are doing this check in "code" unit related pages, so if the code unit is readable, no need to do any further redirection
return
}
diff --git a/services/convert/convert.go b/services/convert/convert.go
index 5df0303646..3b6139d2fe 100644
--- a/services/convert/convert.go
+++ b/services/convert/convert.go
@@ -336,7 +336,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
Description: t.Description,
IncludesAllRepositories: t.IncludesAllRepositories,
CanCreateOrgRepo: t.CanCreateOrgRepo,
- Permission: t.AccessMode.String(),
+ Permission: t.AccessMode.ToString(),
Units: t.GetUnitNames(),
UnitsMap: t.GetUnitsMap(),
}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index 39efd304a9..3b293fe550 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -25,12 +25,13 @@ func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo a
func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository {
var parent *api.Repository
- if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil {
- // If Units and UnitsMode are both nil, it means that it's a hard coded permission,
- // like access_model.Permission{AccessMode: perm.AccessModeAdmin}.
- // So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone.
+ if !permissionInRepo.HasUnits() && permissionInRepo.AccessMode > perm.AccessModeNone {
+ // If units is empty, it means that it's a hard-coded permission, like access_model.Permission{AccessMode: perm.AccessModeAdmin}
+ // So we need to load units for the repo, otherwise UnitAccessMode will just return perm.AccessModeNone.
+ // TODO: this logic is still not right (because unit modes are not correctly prepared)
+ // the caller should prepare a proper "permission" before calling this function.
_ = repo.LoadUnits(ctx) // the error is not important, so ignore it
- permissionInRepo.Units = repo.Units
+ permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode)
}
cloneLink := repo.CloneLink()
diff --git a/services/convert/user.go b/services/convert/user.go
index 1a2733d91e..2957c58b14 100644
--- a/services/convert/user.go
+++ b/services/convert/user.go
@@ -103,7 +103,7 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
return api.RepoCollaboratorPermission{
User: ToUser(ctx, user, doer),
- Permission: accessMode.String(),
- RoleName: accessMode.String(),
+ Permission: accessMode.ToString(),
+ RoleName: accessMode.ToString(),
}
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index e45a2a1695..f49cc2e86b 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -134,6 +134,7 @@ type RepoSettingForm struct {
EnableWiki bool
EnableExternalWiki bool
DefaultWikiBranch string
+ DefaultWikiEveryoneAccess string
ExternalWikiURL string
EnableIssues bool
EnableExternalTracker bool
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 251785d078..c0411cfc56 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -317,7 +317,9 @@
</div>
</div>
- {{$isWikiEnabled := or (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeWiki) (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}}
+ {{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
+ {{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}}
+ {{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}}
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}}
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}}
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}}
@@ -331,21 +333,33 @@
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box">
<div class="field">
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
- <input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-target="#external_wiki_box" {{if not (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}}checked{{end}}>
+ <input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
</div>
</div>
- <div class="inline field tw-pl-4">
- <label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
- <input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
+ <div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}">
+ <div class="inline field">
+ <label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
+ <input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
+ </div>
+ <div class="inline field">
+ {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}}
+ <label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label>
+ <select name="default_wiki_everyone_access" class="ui dropdown">
+ {{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
+ <option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
+ <option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
+ <option value="write" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 2) "selected"}}>{{ctx.Locale.Tr "settings.permission_write"}}</option>
+ </select>
+ </div>
</div>
<div class="field">
<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 ctx.Consts.RepoUnitTypeExternalWiki}}checked{{end}}>
+ <input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
</div>
</div>
- <div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box">
+ <div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}">
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go
index 4df545284e..d14c66ff2c 100644
--- a/tests/integration/api_team_test.go
+++ b/tests/integration/api_team_test.go
@@ -126,7 +126,7 @@ func TestAPITeam(t *testing.T) {
apiTeam = api.Team{}
DecodeJSON(t, resp, &apiTeam)
checkTeamResponse(t, "ReadTeam1", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
- teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
+ teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
@@ -197,7 +197,7 @@ func TestAPITeam(t *testing.T) {
DecodeJSON(t, resp, &apiTeam)
assert.NoError(t, teamRead.LoadUnits(db.DefaultContext))
checkTeamResponse(t, "ReadTeam2", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
- teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
+ teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).