mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Allow everyone to read or write a wiki by a repo unit setting (#30495)
Replace #6312 Help #5833 Wiki solution for #639
This commit is contained in:
		| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										17
									
								
								models/migrations/v1_23/v297.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								models/migrations/v1_23/v297.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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{}) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| 	return len(p.UnitsMode) > 0 | ||||
|  | ||||
| // 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 { | ||||
| 		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 | ||||
| 	// 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 := "<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...) | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 			} | ||||
| 			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) (Permission, error) { | ||||
| 	var perm Permission | ||||
| 	if log.IsTrace() { | ||||
| func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { | ||||
| 	defer func() { | ||||
| 			if user == nil { | ||||
| 				log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v", | ||||
| 					repo, | ||||
| 					perm) | ||||
| 				return | ||||
| 		if err == nil { | ||||
| 			applyEveryoneRepoPermission(user, &perm) | ||||
| 		} | ||||
| 		if log.IsTrace() { | ||||
| 			log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) | ||||
| 		} | ||||
| 			log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+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 { | ||||
|   | ||||
							
								
								
									
										98
									
								
								models/perm/access/repo_permission_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								models/perm/access/repo_permission_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										22
									
								
								models/perm/access_mode_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								models/perm/access_mode_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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")) | ||||
| } | ||||
| @@ -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" | ||||
| @@ -46,6 +47,7 @@ type RepoUnit struct { //revive:disable-line:exported | ||||
| 	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() { | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
| // | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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" | ||||
| @@ -479,6 +480,7 @@ func SettingsPost(ctx *context.Context) { | ||||
| 				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 { | ||||
|   | ||||
| @@ -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 | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -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(), | ||||
| 		} | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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(), | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -134,6 +134,7 @@ type RepoSettingForm struct { | ||||
| 	EnableWiki                            bool | ||||
| 	EnableExternalWiki                    bool | ||||
| 	DefaultWikiBranch                     string | ||||
| 	DefaultWikiEveryoneAccess             string | ||||
| 	ExternalWikiURL                       string | ||||
| 	EnableIssues                          bool | ||||
| 	EnableExternalTracker                 bool | ||||
|   | ||||
| @@ -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"> | ||||
| 					<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> | ||||
|   | ||||
| @@ -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). | ||||
|   | ||||
		Reference in New Issue
	
	Block a user