From 3feba9f1f44156c256a30d25ad1c25f751819c94 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 17 Apr 2024 23:58:37 +0800 Subject: [PATCH] Allow everyone to read or write a wiki by a repo unit setting (#30495) Replace #6312 Help #5833 Wiki solution for #639 --- models/issues/pull_list.go | 6 +- models/migrations/migrations.go | 2 + models/migrations/v1_11/v111.go | 2 +- models/migrations/v1_23/v297.go | 17 +++ models/organization/team.go | 4 +- models/perm/access/access.go | 8 +- models/perm/access/repo_permission.go | 145 ++++++++++++--------- models/perm/access/repo_permission_test.go | 98 ++++++++++++++ models/perm/access_mode.go | 39 +++--- models/perm/access_mode_test.go | 22 ++++ models/repo/repo_unit.go | 12 +- models/unit/unit.go | 11 +- modules/templates/helper.go | 12 ++ options/locale/locale_en-US.ini | 2 + routers/api/v1/api.go | 6 +- routers/private/hook_pre_receive.go | 6 +- routers/web/repo/setting/setting.go | 8 +- routers/web/repo/view.go | 3 +- services/convert/convert.go | 2 +- services/convert/repository.go | 11 +- services/convert/user.go | 4 +- services/forms/repo_form.go | 1 + templates/repo/settings/options.tmpl | 28 +++- tests/integration/api_team_test.go | 4 +- 24 files changed, 322 insertions(+), 131 deletions(-) create mode 100644 models/migrations/v1_23/v297.go create mode 100644 models/perm/access/repo_permission_test.go create mode 100644 models/perm/access_mode_test.go diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index de3eceed3..b5557cad0 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 5326d48f9..cb3a64f48 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 d757acb7d..1722792a3 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 000000000..e79f04cf9 --- /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 501a43d3a..e4e83fede 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 b422a0861..6a0a901f7 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 e4e7579e6..9cce95b77 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 + return len(p.unitsMode) > 0 || p.AccessMode >= perm_model.AccessModeRead } -// UnitAccessMode returns current user accessmode 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 - } - } - return perm_model.AccessModeNone +// 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 0 +} + +// 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 { + // 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) + } + } + // 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 } - return p.UnitsMode[unitType] } // 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 := " 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 000000000..aaa53bb24 --- /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 a37bc1f0e..0364191e2 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("", mode, mode.String()) + return fmt.Sprintf("", 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 000000000..982fceee5 --- /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 5a841f4d3..fd5baa948 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 b216712d3..a78a2f1e4 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 5d2fa79bc..360b48c59 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 ed274197c..a7c1d9179 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 1fc768296..f60c5f21d 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 32ec3003e..4e59237ed 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 00a5282f3..b55e259e4 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 de35c6b3a..9c1f4faa5 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 5df030364..3b6139d2f 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 39efd304a..3b293fe55 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 1a2733d91..2957c58b1 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 e45a2a169..f49cc2e86 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 251785d07..c0411cfc5 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -317,7 +317,9 @@ - {{$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 @@
- +
-
- - +
+
+ + +
+
+ {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} + + +
- +
-
+

{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}

diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go index 4df545284..d14c66ff2 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).