]> source.dussan.org Git - gitea.git/commitdiff
Allow everyone to read or write a wiki by a repo unit setting (#30495)
authorwxiaoguang <wxiaoguang@gmail.com>
Wed, 17 Apr 2024 15:58:37 +0000 (23:58 +0800)
committerGitHub <noreply@github.com>
Wed, 17 Apr 2024 15:58:37 +0000 (15:58 +0000)
Replace #6312
Help #5833
Wiki solution for #639

24 files changed:
models/issues/pull_list.go
models/migrations/migrations.go
models/migrations/v1_11/v111.go
models/migrations/v1_23/v297.go [new file with mode: 0644]
models/organization/team.go
models/perm/access/access.go
models/perm/access/repo_permission.go
models/perm/access/repo_permission_test.go [new file with mode: 0644]
models/perm/access_mode.go
models/perm/access_mode_test.go [new file with mode: 0644]
models/repo/repo_unit.go
models/unit/unit.go
modules/templates/helper.go
options/locale/locale_en-US.ini
routers/api/v1/api.go
routers/private/hook_pre_receive.go
routers/web/repo/setting/setting.go
routers/web/repo/view.go
services/convert/convert.go
services/convert/repository.go
services/convert/user.go
services/forms/repo_form.go
templates/repo/settings/options.tmpl
tests/integration/api_team_test.go

index de3eceed374d73ce89ddd4417f881fd1800926fe..b5557cad060ac6562a714082fd5be49becc7d116 100644 (file)
@@ -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
        }
index 5326d48f901bc92a4e15bdfaf9efa7927ec9e866..cb3a64f48c633eeab74988268b67fc18aeba502f 100644 (file)
@@ -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
index d757acb7d23282bb838328d0102532a3cb115f03..1722792a38f821fe1454223538f220809be3dc15 100644 (file)
@@ -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 (file)
index 0000000..e79f04c
--- /dev/null
@@ -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{})
+}
index 501a43d3a16c9455c9f1be3923ea8b67acd30f69..e4e83fedeeba118193f7eb841f89a167304d1e6c 100644 (file)
@@ -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
index b422a086149a041c42b7542e9e7abcb7058198d1..6a0a901f719e712e3624e0d47a2134a3247298d6 100644 (file)
@@ -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 {
index e4e7579e62bb346b5eb1ba1dcbd5461e5a212b96..9cce95b77648ea6936d24a827829b962097671ce 100644 (file)
@@ -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 (file)
index 0000000..aaa53bb
--- /dev/null
@@ -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")
+}
index a37bc1f0e164e832f7c515308e3b03e244f77b85..0364191e2e9b29121be90a1468dc149cf6e0339c 100644 (file)
@@ -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 (file)
index 0000000..982fcee
--- /dev/null
@@ -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"))
+}
index 5a841f4d312e89f4aa4ac194662ea378313799c8..fd5baa948861c02b9c269bfcc7754798d2254355 100644 (file)
@@ -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() {
index b216712d37c7c313c5cfe3bfc722e4fa0b497ed9..a78a2f1e47125cd759421009705b0e21f7908699 100644 (file)
@@ -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,
        }
 
index 5d2fa79bc524ccd16b9a87df9e2964ff2ebd728f..360b48c59416a6e3295a1b744e30f6493b8af3e9 100644 (file)
@@ -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.
 //
index ed274197c7bb91f5b5fb29e87a14a872432f7d30..a7c1d91791673bd7ef7780ef5fc1eea986357587 100644 (file)
@@ -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
index 1fc768296685fea0add458c1e3daa9eb1fb38d4d..f60c5f21db2060037b45b09519abc781d8bfea9b 100644 (file)
@@ -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 {
index 32ec3003e265cb9516e07c621eae5f3b0cf49262..4e59237ed3ec6f1c5aa5250f2a93ed7d800fe172 100644 (file)
@@ -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 {
index 00a5282f34e772b2256bc5251cde07731db8267b..b55e259e4bea9e266ff6b1e1c7f2779160da1935 100644 (file)
@@ -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 {
index de35c6b3a2590b94806b8d0e394eda45aff3af6a..9c1f4faa5f633f66bbb06d2a99f4c04bde2ba856 100644 (file)
@@ -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
                        }
 
index 5df0303646b1cfdc1ff8f92804cde7de36f00dd3..3b6139d2fec30dfac00b8bb3af8a09cf21649492 100644 (file)
@@ -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(),
                }
index 39efd304a96adcce2b1f23c1323fe664ebc84d88..3b293fe550a38729ba2348494aa6d5ac367459cc 100644 (file)
@@ -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()
index 1a2733d91e63c19c0eda29c7b684c7749c9bbe14..2957c58b14771cde50d14869ed9cbe7a784d21e7 100644 (file)
@@ -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(),
        }
 }
index e45a2a16955224b7610d9694613fef84a1ec8b93..f49cc2e86bcb170ce55ba20000da3645195ad164 100644 (file)
@@ -134,6 +134,7 @@ type RepoSettingForm struct {
        EnableWiki                            bool
        EnableExternalWiki                    bool
        DefaultWikiBranch                     string
+       DefaultWikiEveryoneAccess             string
        ExternalWikiURL                       string
        EnableIssues                          bool
        EnableExternalTracker                 bool
index 251785d078fba6728ae849e5f413d8cf8f3c6a1f..c0411cfc562e9a31e8ee98516e2c4bf5115f00ee 100644 (file)
                                        </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}}
                                <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>
index 4df545284e97f01bb2615590be47904203bb656a..d14c66ff2ca646c39b51aba0f46357651455f2ab 100644 (file)
@@ -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).