mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-05 03:10:12 +00:00
1d43b736b5
### What
`GetActionsUserRepoPermission` (`models/perm/access/repo_permission.go`)
decides whether an Actions task token may access a target repo. Its
cross-repo branches each enforce a fork-PR discriminator — except the
collaborative-owner branch, which was missing the
`!task.IsForkPullRequest` guard that its sibling
`checkSameOwnerCrossRepoAccess` has.
As a result, when a private repo **B** lists owner **A** as a
collaborative owner, an attacker-controlled fork pull-request workflow
whose base repo is owned by A was granted code-read on B — i.e. the
fork's workflow could clone a third private repository it has no rights
to (read-only confidentiality breach).
### Fix
Add the same fork-PR guard the sibling path already enforces:
```go
if taskRepo.IsPrivate && !task.IsForkPullRequest {
actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
return maxPerm, nil
}
}
```
190 lines
7.6 KiB
Go
190 lines
7.6 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package access
|
|
|
|
import (
|
|
"testing"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
"gitea.dev/models/db"
|
|
perm_model "gitea.dev/models/perm"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/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("CollaborativeOwner_ForkPR_Denied", func(t *testing.T) {
|
|
// Target repo15 trusts repo2's owner as a collaborative owner.
|
|
repo15ActionsUnit := repo15.MustGetUnit(ctx, unit.TypeActions)
|
|
repo15ActionsUnit.ActionsConfig().AddCollaborativeOwner(owner2.ID)
|
|
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
|
|
|
|
// Owner cross-repo policy does not allow repo15, so the only branch that
|
|
// could grant access is the collaborative-owner one.
|
|
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, actions_model.OwnerActionsConfig{}))
|
|
|
|
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
|
|
|
// Non-fork task is legitimately granted code-read via collaborative owner.
|
|
task53.IsForkPullRequest = false
|
|
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
|
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, perm.CanRead(unit.TypeCode))
|
|
|
|
// Fork PR must NOT be able to read a third private repo through the
|
|
// collaborative-owner branch.
|
|
task53.IsForkPullRequest = true
|
|
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
|
perm, err = GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, perm.CanRead(unit.TypeCode))
|
|
|
|
// Restore state for subsequent subtests.
|
|
task53.IsForkPullRequest = false
|
|
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
|
|
repo15ActionsUnit.ActionsConfig().RemoveCollaborativeOwner(owner2.ID)
|
|
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
|
|
})
|
|
|
|
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))
|
|
})
|
|
}
|