feat: vendor gitea 1.16.2
This commit is contained in:
@@ -91,43 +91,80 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
|
||||
// refreshAccesses updates the repository's access records in the database by comparing the provided accessMap
|
||||
// with existing records. It minimizes DB operations by performing selective inserts, updates, and deletes
|
||||
// instead of removing all existing records and re-adding them.
|
||||
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
|
||||
minMode := perm.AccessModeRead
|
||||
minModeToKeep := perm.AccessModeRead
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
// If the repo isn't private and isn't owned by a organization,
|
||||
// If the repo isn't private and isn't owned by an organization,
|
||||
// increase the minMode to Write.
|
||||
if !repo.IsPrivate && !repo.Owner.IsOrganization() {
|
||||
minMode = perm.AccessModeWrite
|
||||
minModeToKeep = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
newAccesses := make([]Access, 0, len(accessMap))
|
||||
// Query existing accesses for cross-comparison
|
||||
var existingAccesses []Access
|
||||
if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil {
|
||||
return fmt.Errorf("find existing accesses: %w", err)
|
||||
}
|
||||
existingMap := make(map[int64]perm.AccessMode, len(existingAccesses))
|
||||
for _, a := range existingAccesses {
|
||||
existingMap[a.UserID] = a.Mode
|
||||
}
|
||||
|
||||
var toDelete []int64
|
||||
var toInsert, toUpdate []Access
|
||||
|
||||
// Determine changes
|
||||
for userID, ua := range accessMap {
|
||||
if ua.Mode < minMode && !ua.User.IsRestricted {
|
||||
continue
|
||||
if ua.Mode < minModeToKeep && !ua.User.IsRestricted {
|
||||
// No explicit access record needed (handled by default permissions, e.g., public repo access)
|
||||
if _, exists := existingMap[userID]; exists {
|
||||
toDelete = append(toDelete, userID)
|
||||
}
|
||||
} else {
|
||||
desiredMode := ua.Mode
|
||||
if existingMode, exists := existingMap[userID]; exists {
|
||||
if existingMode != desiredMode {
|
||||
toUpdate = append(toUpdate, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode})
|
||||
}
|
||||
} else {
|
||||
toInsert = append(toInsert, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode})
|
||||
}
|
||||
}
|
||||
|
||||
newAccesses = append(newAccesses, Access{
|
||||
UserID: userID,
|
||||
RepoID: repo.ID,
|
||||
Mode: ua.Mode,
|
||||
})
|
||||
delete(existingMap, userID)
|
||||
}
|
||||
|
||||
// Delete old accesses and insert new ones for repository.
|
||||
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
|
||||
return fmt.Errorf("delete old accesses: %w", err)
|
||||
}
|
||||
if len(newAccesses) == 0 {
|
||||
return nil
|
||||
// Remaining in existingMap should be deleted
|
||||
for userID := range existingMap {
|
||||
toDelete = append(toDelete, userID)
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, newAccesses); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %w", err)
|
||||
// Execute deletions
|
||||
if len(toDelete) > 0 {
|
||||
if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil {
|
||||
return fmt.Errorf("delete accesses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute updates
|
||||
for _, u := range toUpdate {
|
||||
if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil {
|
||||
return fmt.Errorf("update access for user %d: %w", u.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute insertions
|
||||
if len(toInsert) > 0 {
|
||||
if err = db.Insert(ctx, toInsert); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,20 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccessLevel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func TestAccess(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("AccessLevel", testAccessLevel)
|
||||
t.Run("HasAccess", testHasAccess)
|
||||
t.Run("RecalculateAccesses", testRecalculateAccesses)
|
||||
t.Run("RecalculateAccesses2", testRecalculateAccesses2)
|
||||
t.Run("RecalculateAccessesUpdateMode", testRecalculateAccessesUpdateMode)
|
||||
t.Run("RecalculateAccessesRemoveAccess", testRecalculateAccessesRemoveAccess)
|
||||
}
|
||||
|
||||
func testAccessLevel(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
@@ -75,9 +84,7 @@ func TestAccessLevel(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeRead, level)
|
||||
}
|
||||
|
||||
func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testHasAccess(t *testing.T) {
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
// A public repository owned by User 2
|
||||
@@ -101,9 +108,8 @@ func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
func testRecalculateAccesses(t *testing.T) {
|
||||
// test with organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.NoError(t, repo1.LoadOwner(t.Context()))
|
||||
|
||||
@@ -118,9 +124,8 @@ func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeOwner, access.Mode)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
func testRecalculateAccesses2(t *testing.T) {
|
||||
// test with non-organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo1.LoadOwner(t.Context()))
|
||||
|
||||
@@ -132,3 +137,67 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
}
|
||||
|
||||
func testRecalculateAccessesUpdateMode(t *testing.T) {
|
||||
// Test the update path in refreshAccesses optimization
|
||||
// Scenario: User's access mode changes from Read to Write
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(t.Context()))
|
||||
|
||||
// Verify initial access mode
|
||||
_ = db.Insert(t.Context(), &repo_model.Collaboration{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite})
|
||||
_ = db.Insert(t.Context(), &access_model.Access{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite})
|
||||
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err := db.GetEngine(t.Context()).Get(initialAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
initialMode := initialAccess.Mode
|
||||
|
||||
// Change collaboration mode to trigger update path
|
||||
newMode := perm_model.AccessModeAdmin
|
||||
assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode")
|
||||
|
||||
_, err = db.GetEngine(t.Context()).
|
||||
Where("user_id = ? AND repo_id = ?", 4, 4).
|
||||
Cols("mode").
|
||||
Update(&repo_model.Collaboration{Mode: newMode})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Recalculate accesses - should UPDATE existing access, not delete+insert
|
||||
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))
|
||||
|
||||
// Verify access was updated, not deleted and re-inserted
|
||||
updatedAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err = db.GetEngine(t.Context()).Get(updatedAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has, "Access should still exist")
|
||||
assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode")
|
||||
}
|
||||
|
||||
func testRecalculateAccessesRemoveAccess(t *testing.T) {
|
||||
// Test the delete path in refreshAccesses optimization
|
||||
// Scenario: Remove a user's collaboration, access should be deleted
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(t.Context()))
|
||||
|
||||
// Verify initial access exists
|
||||
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err := db.GetEngine(t.Context()).Get(initialAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has, "Access should exist initially")
|
||||
|
||||
// Remove the collaboration to trigger delete path
|
||||
_, err = db.GetEngine(t.Context()).
|
||||
Where("user_id = ? AND repo_id = ?", 4, 4).
|
||||
Delete(&repo_model.Collaboration{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Recalculate accesses - should DELETE the access record
|
||||
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))
|
||||
|
||||
// Verify access was deleted
|
||||
removedAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err = db.GetEngine(t.Context()).Get(removedAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has, "Access should be deleted after removing collaboration")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package access
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetActionsUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
// Use fixtures for repos and users
|
||||
repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // Public, Owner 5, has Actions unit
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // Private, Owner 2, no Actions unit in fixtures
|
||||
repo15 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15}) // Private, Owner 2, no Actions unit in fixtures
|
||||
owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
actionsUser := user_model.NewActionsUser()
|
||||
|
||||
// Ensure repo2 and repo15 have Actions units for testing configuration
|
||||
for _, r := range []*repo_model.Repository{repo2, repo15} {
|
||||
require.NoError(t, db.Insert(ctx, &repo_model.RepoUnit{
|
||||
RepoID: r.ID,
|
||||
Type: unit.TypeActions,
|
||||
Config: &repo_model.ActionsConfig{},
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("SameRepo_Public", func(t *testing.T) {
|
||||
task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
||||
require.Equal(t, repo4.ID, task47.RepoID)
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo4, actionsUser, task47.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Public repo, bot should have Read access even if not collaborator
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.True(t, perm.CanRead(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("SameRepo_Private", func(t *testing.T) {
|
||||
// Use Task 53 which is already in Repo 2 (Private)
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
require.Equal(t, repo2.ID, task53.RepoID)
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Private repo, bot has no base access, but gets Write from effective tokens perms (Permissive by default)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.True(t, perm.CanWrite(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("CrossRepo_Denied_None", func(t *testing.T) {
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
|
||||
// Set owner policy to nil allowed repos (None)
|
||||
cfg := actions_model.OwnerActionsConfig{}
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg))
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should NOT have access to the private repo.
|
||||
assert.False(t, perm.CanRead(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("ForkPR_NoCrossRepo", func(t *testing.T) {
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
task53.IsForkPullRequest = true
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
|
||||
// Policy contains repo15
|
||||
cfg := actions_model.OwnerActionsConfig{
|
||||
AllowedCrossRepoIDs: []int64{repo15.ID},
|
||||
}
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg))
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fork PR never gets cross-repo access to other private repos
|
||||
assert.False(t, perm.CanRead(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("Inheritance_And_Clamping", func(t *testing.T) {
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
task53.IsForkPullRequest = false
|
||||
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
||||
|
||||
// Owner policy: Restricted mode (Read-only Code)
|
||||
ownerCfg := actions_model.OwnerActionsConfig{
|
||||
TokenPermissionMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
MaxTokenPermissions: &repo_model.ActionsTokenPermissions{
|
||||
UnitAccessModes: map[unit.Type]perm_model.AccessMode{
|
||||
unit.TypeCode: perm_model.AccessModeRead,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg))
|
||||
|
||||
// Repo policy: OverrideOwnerConfig = false (should inherit owner's restricted mode)
|
||||
repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions)
|
||||
repo2ActionsCfg := repo2ActionsUnit.ActionsConfig()
|
||||
repo2ActionsCfg.OverrideOwnerConfig = false
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit))
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be clamped to Read-only
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode))
|
||||
assert.False(t, perm.CanWrite(unit.TypeCode))
|
||||
})
|
||||
|
||||
t.Run("RepoOverride_Clamping", func(t *testing.T) {
|
||||
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
|
||||
// Owner policy: Permissive (Write access)
|
||||
ownerCfg := actions_model.OwnerActionsConfig{
|
||||
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
}
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg))
|
||||
|
||||
// Repo policy: OverrideOwnerConfig = true, MaxTokenPermissions = Read
|
||||
repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions)
|
||||
repo2ActionsCfg := repo2ActionsUnit.ActionsConfig()
|
||||
repo2ActionsCfg.OverrideOwnerConfig = true
|
||||
repo2ActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModeRestricted
|
||||
repo2ActionsCfg.MaxTokenPermissions = &repo_model.ActionsTokenPermissions{
|
||||
UnitAccessModes: map[unit.Type]perm_model.AccessMode{
|
||||
unit.TypeCode: perm_model.AccessModeRead,
|
||||
},
|
||||
}
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit))
|
||||
|
||||
perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be clamped to Read-only
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode))
|
||||
})
|
||||
}
|
||||
@@ -5,9 +5,13 @@ package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
@@ -167,7 +171,8 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
|
||||
}
|
||||
|
||||
func (p *Permission) LogString() string {
|
||||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): ["
|
||||
var format strings.Builder
|
||||
format.WriteString("<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [")
|
||||
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
|
||||
|
||||
for i, u := range p.units {
|
||||
@@ -179,19 +184,19 @@ func (p *Permission) LogString() string {
|
||||
config = err.Error()
|
||||
}
|
||||
}
|
||||
format += "\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s"
|
||||
format.WriteString("\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s")
|
||||
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
|
||||
}
|
||||
for key, value := range p.unitsMode {
|
||||
format += "\n\tunitsMode[%-v]: %-v"
|
||||
format.WriteString("\n\tunitsMode[%-v]: %-v")
|
||||
args = append(args, key.LogString(), value.LogString())
|
||||
}
|
||||
format += "\n\tanonymousAccessMode: %-v"
|
||||
format.WriteString("\n\tanonymousAccessMode: %-v")
|
||||
args = append(args, p.anonymousAccessMode)
|
||||
format += "\n\teveryoneAccessMode: %-v"
|
||||
format.WriteString("\n\teveryoneAccessMode: %-v")
|
||||
args = append(args, p.everyoneAccessMode)
|
||||
format += "\n\t]>"
|
||||
return fmt.Sprintf(format, args...)
|
||||
format.WriteString("\n\t]>")
|
||||
return fmt.Sprintf(format.String(), args...)
|
||||
}
|
||||
|
||||
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
|
||||
@@ -254,8 +259,139 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func checkSameOwnerCrossRepoAccess(ctx context.Context, taskRepo, targetRepo *repo_model.Repository, isForkPR bool) bool {
|
||||
if isForkPR {
|
||||
// Fork PRs are never allowed cross-repo access to other private repositories of the owner.
|
||||
return false
|
||||
}
|
||||
if taskRepo.OwnerID != targetRepo.OwnerID {
|
||||
return false
|
||||
}
|
||||
ownerCfg, err := actions_model.GetOwnerActionsConfig(ctx, targetRepo.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("GetOwnerActionsConfig: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return slices.Contains(ownerCfg.AllowedCrossRepoIDs, targetRepo.ID)
|
||||
}
|
||||
|
||||
// GetActionsUserRepoPermission returns the actions user permissions to the repository
|
||||
func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) {
|
||||
if actionsUser.ID != user_model.ActionsUserID {
|
||||
return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user")
|
||||
}
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
if err := task.LoadJob(ctx); err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
var taskRepo *repo_model.Repository
|
||||
if task.RepoID != repo.ID {
|
||||
if err := task.Job.LoadRepo(ctx); err != nil {
|
||||
return perm, err
|
||||
}
|
||||
taskRepo = task.Job.Repo
|
||||
} else {
|
||||
taskRepo = repo
|
||||
}
|
||||
|
||||
// Compute effective permissions for this task against the target repo
|
||||
effectivePerms, err := actions_model.ComputeTaskTokenPermissions(ctx, task, repo)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
if task.RepoID != repo.ID {
|
||||
// Cross-repo access must also respect the target repo's permission ceiling.
|
||||
targetRepoActionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if targetRepoActionsCfg.OverrideOwnerConfig {
|
||||
effectivePerms = targetRepoActionsCfg.ClampPermissions(effectivePerms)
|
||||
} else {
|
||||
targetRepoOwnerActionsCfg, err := actions_model.GetOwnerActionsConfig(ctx, repo.OwnerID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
effectivePerms = targetRepoOwnerActionsCfg.ClampPermissions(effectivePerms)
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo.LoadUnits(ctx); err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
var maxPerm Permission
|
||||
|
||||
// Set up per-unit access modes based on configured permissions
|
||||
maxPerm.units = repo.Units
|
||||
maxPerm.unitsMode = maps.Clone(effectivePerms.UnitAccessModes)
|
||||
|
||||
// Check permission like simple user but limit to read-only (PR #36095)
|
||||
// Enhanced to also grant read-only access if isSameRepo is true and target repository is public
|
||||
botPerm, err := GetIndividualUserRepoPermission(ctx, repo, user_model.NewActionsUser())
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
if botPerm.AccessMode >= perm_model.AccessModeRead {
|
||||
// Public repo allows read access, increase permissions to at least read
|
||||
// Otherwise you cannot access your own repository if your permissions are set to none but the repository is public
|
||||
for _, u := range repo.Units {
|
||||
if botPerm.CanRead(u.Type) {
|
||||
maxPerm.unitsMode[u.Type] = max(maxPerm.unitsMode[u.Type], perm_model.AccessModeRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if task.RepoID == repo.ID {
|
||||
return maxPerm, nil
|
||||
}
|
||||
|
||||
if checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) {
|
||||
// Access allowed by owner policy (grants access to private repos).
|
||||
// Note: maxPerm has already been restricted to Read-Only in ComputeTaskTokenPermissions
|
||||
// because isSameRepo is false.
|
||||
return maxPerm, nil
|
||||
}
|
||||
|
||||
// Fall through to allow public repository read access via botPerm check below
|
||||
|
||||
// Check if the repo is public or the Bot has explicit access
|
||||
if botPerm.AccessMode >= perm_model.AccessModeRead {
|
||||
return maxPerm, nil
|
||||
}
|
||||
|
||||
// Check Collaborative Owner and explicit Bot permissions
|
||||
// We allow access if:
|
||||
// 1. It's a collaborative owner relationship
|
||||
// 2. The Actions Bot user has been explicitly granted access and repository is private
|
||||
// 3. The repository is public (handled by botPerm above)
|
||||
|
||||
if taskRepo.IsPrivate {
|
||||
actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||
if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
|
||||
return maxPerm, nil
|
||||
}
|
||||
}
|
||||
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// GetDoerRepoPermission returns the repository permission for the current actor,
|
||||
// dispatching to GetActionsUserRepoPermission when the actor is an Actions token user.
|
||||
func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) {
|
||||
if taskID, ok := user_model.GetActionsUserTaskID(user); ok {
|
||||
return GetActionsUserRepoPermission(ctx, repo, user, taskID)
|
||||
}
|
||||
return GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
}
|
||||
|
||||
// GetIndividualUserRepoPermission returns the permissions for an explicit user identity.
|
||||
// In most request paths, callers should use GetDoerRepoPermission instead.
|
||||
// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users.
|
||||
func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
@@ -414,8 +550,9 @@ func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
|
||||
|
||||
// AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the
|
||||
// user does not have access.
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return perm_model.AccessModeNone, err
|
||||
}
|
||||
@@ -434,7 +571,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
|
||||
if user.IsOrganization() {
|
||||
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)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -443,6 +580,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
|
||||
}
|
||||
|
||||
// HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess"
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) {
|
||||
var user *user_model.User
|
||||
var err error
|
||||
@@ -452,7 +590,7 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -500,13 +638,14 @@ func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository,
|
||||
}
|
||||
|
||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
|
||||
if user != nil && user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission: %w", err)
|
||||
log.Error("GetIndividualUserRepoPermission: %w", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package access
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
@@ -157,8 +158,13 @@ func TestUnitAccessMode(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
|
||||
}
|
||||
|
||||
func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func TestGetRepoPermission(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("GetIndividualUserRepoPermission", testGetIndividualUserRepoPermission)
|
||||
t.Run("GetDoerRepoPermission", testGetDoerRepoPermission)
|
||||
}
|
||||
|
||||
func testGetIndividualUserRepoPermission(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
|
||||
require.NoError(t, repo32.LoadOwner(ctx))
|
||||
@@ -172,7 +178,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
|
||||
@@ -181,7 +187,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
|
||||
@@ -191,7 +197,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
@@ -204,7 +210,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}, &Access{})) // The user has access set of that repo, remove it, it is useless for our test
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo3.ID}))
|
||||
t.Run("DoerWithNoopTeamOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
@@ -214,7 +220,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeIssues, AccessMode: perm_model.AccessModeRead}))
|
||||
t.Run("DoerWithReadIssueTeamOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
@@ -233,7 +239,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
require.NoError(t, db.Insert(ctx, Access{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithReadIssueTeamAndWriteCollaboratorOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
@@ -245,3 +251,25 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetDoerRepoPermission(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
||||
actionsDoer := user_model.NewActionsUserWithTaskID(task47.ID)
|
||||
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actionsPerm, err := GetDoerRepoPermission(ctx, repo4, actionsDoer)
|
||||
require.NoError(t, err)
|
||||
directPerm, err := GetActionsUserRepoPermission(ctx, repo4, actionsDoer, task47.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, directPerm, actionsPerm)
|
||||
|
||||
doerPerm, err := GetDoerRepoPermission(ctx, repo1, regularUser)
|
||||
require.NoError(t, err)
|
||||
individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, individualPerm, doerPerm)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user