Skip to content

Commit

Permalink
Allow everyone to read or write a wiki by a repo unit setting (#30495)
Browse files Browse the repository at this point in the history
Replace #6312
Help #5833
Wiki solution for #639
  • Loading branch information
wxiaoguang authored Apr 17, 2024
1 parent bafb80f commit 3feba9f
Show file tree
Hide file tree
Showing 24 changed files with 319 additions and 128 deletions.
6 changes: 4 additions & 2 deletions models/issues/pull_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion models/migrations/v1_11/v111.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v1_23/v297.go
Original file line number Diff line number Diff line change
@@ -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{})
}
4 changes: 2 additions & 2 deletions models/organization/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions models/perm/access/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
139 changes: 81 additions & 58 deletions models/perm/access/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package access
import (
"context"
"fmt"
"slices"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -114,45 +136,56 @@ 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())
}
format += " ]>"
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???
Expand All @@ -162,15 +195,14 @@ 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 {
return perm, err
}
}

if err := repo.LoadOwner(ctx); err != nil {
if err = repo.LoadOwner(ctx); err != nil {
return perm, err
}

Expand All @@ -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
Expand All @@ -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
}
}

Expand All @@ -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
}
}
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 3feba9f

Please sign in to comment.