forked from mirrors/forgejo
feat: trust management for runs created from a forked pull request
- UpdateTrustedWithPullRequest - cancels or approves runs and keep track of posters that are to always be trusted - GetPullRequestUserIsTrustedWithActions - logic to determine if a user is to be implicitly trusted (e.g. the admin of the instance), explicitly trusted (i.e. it is in the ActionUser table) or not at all. - PullRequestCancel & PullRequestApprove will either cancel or approve all runs of a given pull request. - RevokeTrust is almost the same as PullRequestCancel except it operates as if revoking all pull requests of a given poster, cancelling ongoing jobs. This is expected to be used when blocking a user. - AlwaysTrust is almost the same as PullRequestApprove except it operates as if allways approving all pull requests of a given poster, switching their jobs to waiting. - SetRunTrustForPullRequest helper to set the fields of ActionRun - CleanupActionUser - get rid of unused trust records
This commit is contained in:
parent
71439965d6
commit
e6522c1ecc
6 changed files with 869 additions and 0 deletions
|
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
id: 10
|
||||
repo_id: 10
|
||||
user_id: 4
|
||||
trusted_with_pull_requests: true
|
||||
last_access: 1683636528
|
||||
-
|
||||
id: 11
|
||||
repo_id: 10
|
||||
user_id: 29
|
||||
trusted_with_pull_requests: true
|
||||
last_access: 1683636528
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
-
|
||||
id: 81000
|
||||
repo_id: 10
|
||||
index: 1000
|
||||
poster_id: 4 # regular user
|
||||
original_author_id: 0
|
||||
name: pr1000
|
||||
content: pull request
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
num_comments: 0
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
is_locked: false
|
||||
|
||||
-
|
||||
id: 82000
|
||||
repo_id: 11
|
||||
index: 2000
|
||||
poster_id: 2 # regular user
|
||||
original_author_id: 0
|
||||
name: pr2000
|
||||
content: pull request
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
num_comments: 0
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
is_locked: false
|
||||
|
||||
-
|
||||
id: 83000
|
||||
repo_id: 10
|
||||
index: 3000
|
||||
poster_id: 1 # admin
|
||||
original_author_id: 0
|
||||
name: pr3000
|
||||
content: pull request
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
num_comments: 0
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
is_locked: false
|
||||
|
||||
-
|
||||
id: 84000
|
||||
repo_id: 10
|
||||
index: 4000
|
||||
poster_id: 5 # regular user
|
||||
original_author_id: 0
|
||||
name: pr4000
|
||||
content: pull request
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
num_comments: 0
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
is_locked: false
|
||||
|
||||
-
|
||||
id: 85000
|
||||
repo_id: 10
|
||||
index: 5000
|
||||
poster_id: 29 # restricted user
|
||||
original_author_id: 0
|
||||
name: pr5000
|
||||
content: pull request
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
num_comments: 0
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
is_locked: false
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
-
|
||||
id: 1000
|
||||
type: 0 # pull request
|
||||
status: 2 # mergeable
|
||||
issue_id: 81000
|
||||
index: 1000
|
||||
head_repo_id: 11
|
||||
base_repo_id: 10
|
||||
head_branch: branch2000
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 2000
|
||||
type: 0 # pull request
|
||||
status: 2 # mergeable
|
||||
issue_id: 82000
|
||||
index: 2000
|
||||
head_repo_id: 11
|
||||
base_repo_id: 11
|
||||
head_branch: branch2000
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 3000
|
||||
type: 0 # pull request
|
||||
status: 2 # mergeable
|
||||
issue_id: 83000
|
||||
index: 3000
|
||||
head_repo_id: 11 # different from base_repo
|
||||
base_repo_id: 10
|
||||
head_branch: branch3000
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 4000
|
||||
type: 0 # pull request
|
||||
status: 2 # mergeable
|
||||
issue_id: 84000
|
||||
index: 4000
|
||||
head_repo_id: 11 # different from base_repo
|
||||
base_repo_id: 10
|
||||
head_branch: branch4000
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
|
||||
-
|
||||
id: 5000
|
||||
type: 0 # pull request
|
||||
status: 2 # mergeable
|
||||
issue_id: 85000
|
||||
index: 5000
|
||||
head_repo_id: 11 # different from base_repo
|
||||
base_repo_id: 10
|
||||
head_branch: branch5000
|
||||
base_branch: master
|
||||
merge_base: 0abcb056019adb83
|
||||
has_merged: false
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-
|
||||
id: 11500
|
||||
repo_id: 10
|
||||
type: 10 # TypeActions
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
326
services/actions/trust.go
Normal file
326
services/actions/trust.go
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
actions_module "forgejo.org/modules/actions"
|
||||
"forgejo.org/modules/log"
|
||||
webhook_module "forgejo.org/modules/webhook"
|
||||
)
|
||||
|
||||
type TrustUpdate string
|
||||
|
||||
const (
|
||||
UserTrustDenied = TrustUpdate("deny")
|
||||
UserAlwaysTrusted = TrustUpdate("always")
|
||||
UserTrustedOnce = TrustUpdate("once")
|
||||
UserTrustRevoked = TrustUpdate("revoke")
|
||||
)
|
||||
|
||||
func CleanupActionUser(ctx context.Context) error {
|
||||
return actions_model.RevokeInactiveActionUser(ctx)
|
||||
}
|
||||
|
||||
func loadPullRequestAttributes(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pr.Issue.LoadRepo(ctx)
|
||||
}
|
||||
|
||||
func getIssuePoster(ctx context.Context, issue *issues_model.Issue) (*user_model.User, error) {
|
||||
if issue.Poster != nil {
|
||||
return issue.Poster, nil
|
||||
}
|
||||
if issue.PosterID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
poster, err := user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("getIssuePoster [%d]: %w", issue.PosterID, err)
|
||||
}
|
||||
issue.Poster = poster
|
||||
return poster, nil
|
||||
}
|
||||
|
||||
func mustGetIssuePoster(ctx context.Context, issue *issues_model.Issue) (*user_model.User, error) {
|
||||
poster, err := getIssuePoster(ctx, issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if poster == nil {
|
||||
return nil, user_model.ErrUserNotExist{UID: issue.PosterID}
|
||||
}
|
||||
return poster, nil
|
||||
}
|
||||
|
||||
type useHeadOrBaseCommit int
|
||||
|
||||
const (
|
||||
useHeadCommit = 1 << iota
|
||||
useBaseCommit
|
||||
)
|
||||
|
||||
func getPullRequestCommitAndApproval(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, event webhook_module.HookEventType) (useHeadOrBaseCommit, actions_model.ApprovalType, error) {
|
||||
if pr == nil || actions_module.IsDefaultBranchWorkflow(event) || !pr.IsForkPullRequest() {
|
||||
return useHeadCommit, actions_model.DoesNotNeedApproval, nil
|
||||
}
|
||||
|
||||
posterTrust, err := GetPullRequestPosterIsTrustedWithActions(ctx, pr)
|
||||
if err != nil {
|
||||
return useHeadCommit, actions_model.UndefinedApproval, err
|
||||
}
|
||||
|
||||
if posterTrust.IsTrusted() {
|
||||
return useHeadCommit, actions_model.DoesNotNeedApproval, nil
|
||||
}
|
||||
|
||||
doerTrust, err := getPullRequestUserIsTrustedWithActions(ctx, pr, doer)
|
||||
if err != nil {
|
||||
return useHeadCommit, actions_model.UndefinedApproval, err
|
||||
}
|
||||
|
||||
if doerTrust.IsTrusted() {
|
||||
if event == webhook_module.HookEventPullRequestSync {
|
||||
// a synchronized event action (i.e. the doer pushed a commit to the pull request)
|
||||
// can run from the head
|
||||
return useHeadCommit, actions_model.DoesNotNeedApproval, nil
|
||||
}
|
||||
// other events run from workflows found in the base, not
|
||||
// from possibly modified workflows found in the head
|
||||
return useBaseCommit, actions_model.DoesNotNeedApproval, nil
|
||||
}
|
||||
// the poster and the doer are not trusted, approval is needed
|
||||
return useHeadCommit, actions_model.NeedApproval, nil
|
||||
}
|
||||
|
||||
// cancels or approves runs and keep track of posters that are to always be trusted
|
||||
func UpdateTrustedWithPullRequest(ctx context.Context, doerID int64, pr *issues_model.PullRequest, trusted TrustUpdate) error {
|
||||
if err := loadPullRequestAttributes(ctx, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch trusted {
|
||||
case UserAlwaysTrusted:
|
||||
poster, err := mustGetIssuePoster(ctx, pr.Issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return AlwaysTrust(ctx, doerID, pr.Issue.RepoID, poster.ID)
|
||||
case UserTrustedOnce:
|
||||
return pullRequestApprove(ctx, doerID, pr.Issue.RepoID, pr.ID)
|
||||
case UserTrustRevoked:
|
||||
poster, err := mustGetIssuePoster(ctx, pr.Issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RevokeTrust(ctx, pr.Issue.RepoID, poster.ID)
|
||||
case UserTrustDenied:
|
||||
return pullRequestCancel(ctx, pr.Issue.RepoID, pr.ID)
|
||||
default:
|
||||
return fmt.Errorf("UpdateTrustedWithPullRequest: unknown trust %v", trusted)
|
||||
}
|
||||
}
|
||||
|
||||
func setRunTrustForPullRequest(ctx context.Context, run *actions_model.ActionRun, pr *issues_model.PullRequest, needApproval actions_model.ApprovalType) error {
|
||||
if pr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := loadPullRequestAttributes(ctx, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
run.IsForkPullRequest = pr.IsForkPullRequest()
|
||||
run.PullRequestPosterID = pr.Issue.PosterID
|
||||
run.PullRequestID = pr.ID
|
||||
run.NeedApproval = bool(needApproval)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserTrust string
|
||||
|
||||
const (
|
||||
UserTrustIsNotRelevant = UserTrust("irrelevant")
|
||||
UserIsNotTrustedWithActions = UserTrust("no")
|
||||
UserIsExplicitlyTrustedWithActions = UserTrust("explicitly")
|
||||
UserIsImplicitlyTrustedWithActions = UserTrust("implicitly")
|
||||
)
|
||||
|
||||
func (t UserTrust) IsTrusted() bool {
|
||||
return t != UserIsNotTrustedWithActions
|
||||
}
|
||||
|
||||
func GetPullRequestPosterIsTrustedWithActions(ctx context.Context, pr *issues_model.PullRequest) (UserTrust, error) {
|
||||
if err := loadPullRequestAttributes(ctx, pr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
poster, err := mustGetIssuePoster(ctx, pr.Issue)
|
||||
if err != nil {
|
||||
return UserIsNotTrustedWithActions, err
|
||||
}
|
||||
|
||||
return getPullRequestUserIsTrustedWithActions(ctx, pr, poster)
|
||||
}
|
||||
|
||||
func getPullRequestUserIsTrustedWithActions(ctx context.Context, pr *issues_model.PullRequest, user *user_model.User) (UserTrust, error) {
|
||||
if err := loadPullRequestAttributes(ctx, pr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userIsTrustedWithPullRequest(ctx, pr, user)
|
||||
}
|
||||
|
||||
func userIsTrustedWithPullRequest(ctx context.Context, pr *issues_model.PullRequest, user *user_model.User) (UserTrust, error) {
|
||||
implicitlyTrusted, err := userIsImplicitlyTrustedWithPullRequest(ctx, pr, user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if implicitlyTrusted {
|
||||
log.Trace("%s is implicitly trusted to run actions in repository %s", user, pr.Issue.Repo)
|
||||
return UserIsImplicitlyTrustedWithActions, nil
|
||||
}
|
||||
|
||||
explicitlyTrusted, err := userIsExplicitlyTrustedWithPullRequest(ctx, pr, user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if explicitlyTrusted {
|
||||
log.Trace("%s is explicitly trusted to run actions in repository %s", user, pr.Issue.Repo)
|
||||
return UserIsExplicitlyTrustedWithActions, nil
|
||||
}
|
||||
|
||||
log.Trace("%s is not trusted to run actions in repository %s", user, pr.Issue.Repo)
|
||||
return UserIsNotTrustedWithActions, nil
|
||||
}
|
||||
|
||||
func userIsImplicitlyTrustedWithPullRequest(ctx context.Context, pr *issues_model.PullRequest, user *user_model.User) (bool, error) {
|
||||
// users that are trusted to create a pull request that is not from a fork
|
||||
// are also implicitly trusted to run workflows
|
||||
if !pr.IsForkPullRequest() {
|
||||
log.Trace("a pull request that is not from a fork nor AGit is implicitly trusted to run actions")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return userCanWriteActionsOnRepo(ctx, pr.Issue.Repo, user)
|
||||
}
|
||||
|
||||
func userCanWriteActionsOnRepo(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) {
|
||||
// users with write permission to the actions unit are trusted to
|
||||
// run actions
|
||||
permission, err := access_model.GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if permission.CanWrite(unit_model.TypeActions) {
|
||||
log.Trace("%s has write permissions to the Action unit on %s", user, repo)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func userIsExplicitlyTrustedWithPullRequest(ctx context.Context, pr *issues_model.PullRequest, user *user_model.User) (bool, error) {
|
||||
// there is no need to check if the user is blocked because it is not
|
||||
// allowed to create a pull request
|
||||
if user.IsRestricted {
|
||||
log.Trace("%v is restricted and cannot be trusted with pull requests", user)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
actionUser, err := actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(ctx, user.ID, pr.Issue.Repo.ID)
|
||||
if err != nil {
|
||||
log.Trace("%v is not explicitly trusted with pull requests on repository %v", user, pr.Issue.Repo)
|
||||
if actions_model.IsErrUserNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Trace("%v is explicitly trusted with pull requests on repository %v", user, pr.Issue.Repo)
|
||||
return actionUser.TrustedWithPullRequests, nil
|
||||
}
|
||||
|
||||
func RevokeTrust(ctx context.Context, repoID, posterID int64) error {
|
||||
if err := actions_model.DeleteActionUserByUserIDAndRepoID(ctx, posterID, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runs, err := actions_model.GetRunsNotDoneByRepoIDAndPullRequestPosterID(ctx, repoID, posterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
if err := CancelRun(ctx, run); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AlwaysTrust(ctx context.Context, doerID, repoID, posterID int64) error {
|
||||
if err := actions_model.InsertActionUser(ctx, &actions_model.ActionUser{
|
||||
UserID: posterID,
|
||||
RepoID: repoID,
|
||||
TrustedWithPullRequests: true,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runs, err := actions_model.GetRunsNotDoneByRepoIDAndPullRequestPosterID(ctx, repoID, posterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
if err := ApproveRun(ctx, run, doerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullRequestCancel(ctx context.Context, repoID, pullRequestID int64) error {
|
||||
runs, err := actions_model.GetRunsNotDoneByRepoIDAndPullRequestID(ctx, repoID, pullRequestID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
if err := CancelRun(ctx, run); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullRequestApprove(ctx context.Context, doerID, repoID, pullRequestID int64) error {
|
||||
runs, err := actions_model.GetRunsThatNeedApprovalByRepoIDAndPullRequestID(ctx, repoID, pullRequestID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
if err := ApproveRun(ctx, run, doerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
377
services/actions/trust_test.go
Normal file
377
services/actions/trust_test.go
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
actions_module "forgejo.org/modules/actions"
|
||||
webhook_module "forgejo.org/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsTrust_ChangeStatus(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repoID := int64(10)
|
||||
pullRequestPosterID := int64(30)
|
||||
|
||||
runDone := &actions_model.ActionRun{
|
||||
RepoID: repoID,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
Status: actions_model.StatusSuccess,
|
||||
}
|
||||
require.NoError(t, actions_model.InsertRun(t.Context(), runDone, nil))
|
||||
|
||||
runNotByPoster := &actions_model.ActionRun{
|
||||
RepoID: repoID,
|
||||
PullRequestPosterID: 43243,
|
||||
Status: actions_model.StatusRunning,
|
||||
}
|
||||
require.NoError(t, actions_model.InsertRun(t.Context(), runNotByPoster, nil))
|
||||
|
||||
runNotInTheSameRepository := &actions_model.ActionRun{
|
||||
RepoID: 5,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
Status: actions_model.StatusSuccess,
|
||||
}
|
||||
require.NoError(t, actions_model.InsertRun(t.Context(), runNotInTheSameRepository, nil))
|
||||
|
||||
t.Run("RevokeTrust", func(t *testing.T) {
|
||||
singleWorkflows, err := actions_module.JobParser([]byte(`
|
||||
jobs:
|
||||
job:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo OK
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, singleWorkflows, 1)
|
||||
runNotDone := &actions_model.ActionRun{
|
||||
TriggerUserID: 2,
|
||||
RepoID: repoID,
|
||||
Status: actions_model.StatusWaiting,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
}
|
||||
require.NoError(t, actions_model.InsertRun(t.Context(), runNotDone, singleWorkflows))
|
||||
require.NoError(t, actions_model.InsertActionUser(t.Context(), &actions_model.ActionUser{
|
||||
UserID: pullRequestPosterID,
|
||||
RepoID: repoID,
|
||||
TrustedWithPullRequests: true,
|
||||
}))
|
||||
|
||||
previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), pullRequestPosterID, repoID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, RevokeTrust(t.Context(), repoID, pullRequestPosterID))
|
||||
|
||||
_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), pullRequestPosterID, repoID)
|
||||
assert.True(t, actions_model.IsErrUserNotExist(err))
|
||||
currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotDone.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
})
|
||||
|
||||
createPullRequestRun := func(t *testing.T, pullRequestID, repoID int64) *actions_model.ActionRun {
|
||||
t.Helper()
|
||||
singleWorkflows, err := actions_module.JobParser([]byte(`
|
||||
jobs:
|
||||
job:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo OK
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, singleWorkflows, 1)
|
||||
runNotApproved := &actions_model.ActionRun{
|
||||
TriggerUserID: 2,
|
||||
RepoID: repoID,
|
||||
Status: actions_model.StatusWaiting,
|
||||
NeedApproval: true,
|
||||
PullRequestID: pullRequestID,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
}
|
||||
require.NoError(t, actions_model.InsertRun(t.Context(), runNotApproved, singleWorkflows))
|
||||
return runNotApproved
|
||||
}
|
||||
|
||||
t.Run("PullRequestCancel", func(t *testing.T) {
|
||||
pullRequestID := int64(485)
|
||||
runNotApproved := createPullRequestRun(t, pullRequestID, repoID)
|
||||
|
||||
previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
|
||||
require.NoError(t, pullRequestCancel(t.Context(), repoID, pullRequestID))
|
||||
|
||||
currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
})
|
||||
|
||||
t.Run("UpdateTrustedWithPullRequest deny", func(t *testing.T) {
|
||||
pullRequestID := int64(485)
|
||||
runNotApproved := createPullRequestRun(t, pullRequestID, repoID)
|
||||
|
||||
previousCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
|
||||
require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), 0, &issues_model.PullRequest{
|
||||
ID: pullRequestID,
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: repoID,
|
||||
},
|
||||
}, UserTrustDenied))
|
||||
|
||||
currentCancelledCount := unittest.GetCount(t, &actions_model.ActionRun{Status: actions_model.StatusCancelled})
|
||||
assert.Equal(t, previousCancelledCount+1, currentCancelledCount)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
})
|
||||
|
||||
t.Run("PullRequestApprove", func(t *testing.T) {
|
||||
pullRequestID := int64(534)
|
||||
runNotApproved := createPullRequestRun(t, pullRequestID, repoID)
|
||||
|
||||
previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
|
||||
doerID := int64(84322)
|
||||
require.NoError(t, pullRequestApprove(t.Context(), doerID, repoID, pullRequestID))
|
||||
|
||||
currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
assert.Equal(t, previousWaitingCount+1, currentWaitingCount)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
assert.Equal(t, doerID, run.ApprovedBy)
|
||||
assert.False(t, run.NeedApproval)
|
||||
})
|
||||
|
||||
t.Run("UpdateTrustedWithPullRequest once", func(t *testing.T) {
|
||||
pullRequestID := int64(534)
|
||||
runNotApproved := createPullRequestRun(t, pullRequestID, repoID)
|
||||
|
||||
previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
|
||||
doerID := int64(84322)
|
||||
require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
|
||||
ID: pullRequestID,
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: repoID,
|
||||
},
|
||||
}, UserTrustedOnce))
|
||||
|
||||
currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
assert.Equal(t, previousWaitingCount+1, currentWaitingCount)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runNotApproved.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
assert.Equal(t, doerID, run.ApprovedBy)
|
||||
assert.False(t, run.NeedApproval)
|
||||
})
|
||||
|
||||
t.Run("UpdateTrustedWithPullRequest always", func(t *testing.T) {
|
||||
pullRequestIDs := []int64{534, 645}
|
||||
var runsNotApproved []*actions_model.ActionRun
|
||||
for _, pullRequestID := range pullRequestIDs {
|
||||
runsNotApproved = append(runsNotApproved, createPullRequestRun(t, pullRequestID, repoID))
|
||||
}
|
||||
|
||||
previousWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
|
||||
doerID := int64(84322)
|
||||
require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
|
||||
ID: pullRequestIDs[0],
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: repoID,
|
||||
PosterID: pullRequestPosterID,
|
||||
},
|
||||
}, UserAlwaysTrusted))
|
||||
|
||||
currentWaitingCount := unittest.GetCount(t, &actions_model.ActionRunJob{Status: actions_model.StatusWaiting})
|
||||
assert.Equal(t, previousWaitingCount+len(pullRequestIDs), currentWaitingCount)
|
||||
|
||||
for _, run := range runsNotApproved {
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusWaiting.String(), run.Status.String())
|
||||
assert.Equal(t, doerID, run.ApprovedBy)
|
||||
assert.False(t, run.NeedApproval)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdateTrustedWithPullRequest revoke", func(t *testing.T) {
|
||||
pullRequestIDs := []int64{748, 953}
|
||||
var runsNotApproved []*actions_model.ActionRun
|
||||
for _, pullRequestID := range pullRequestIDs {
|
||||
runsNotApproved = append(runsNotApproved, createPullRequestRun(t, pullRequestID, repoID))
|
||||
}
|
||||
|
||||
doerID := int64(84322)
|
||||
require.NoError(t, UpdateTrustedWithPullRequest(t.Context(), doerID, &issues_model.PullRequest{
|
||||
ID: pullRequestIDs[0],
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: repoID,
|
||||
PosterID: pullRequestPosterID,
|
||||
},
|
||||
}, UserTrustRevoked))
|
||||
|
||||
for _, run := range runsNotApproved {
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled.String(), run.Status.String())
|
||||
assert.False(t, run.NeedApproval)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionsTrust_GetPullRequestUserIsTrustedWithActions(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestActionsTrust_GetPullRequestUserIsTrustedWithActions")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("implicitly trusted because the pull request is not from a fork", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2000})
|
||||
trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.False(t, pr.IsForkPullRequest())
|
||||
assert.Equal(t, UserIsImplicitlyTrustedWithActions, trust)
|
||||
})
|
||||
|
||||
t.Run("implicitly trusted on a forked pull request when the poster is admin", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
|
||||
trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.True(t, pr.IsForkPullRequest())
|
||||
require.True(t, pr.Issue.Poster.IsAdmin)
|
||||
assert.Equal(t, UserIsImplicitlyTrustedWithActions, trust)
|
||||
})
|
||||
|
||||
t.Run("explicitly trusted on a forked pull request when the poster was permanently approved", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1000})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // regular user
|
||||
trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.True(t, pr.IsForkPullRequest())
|
||||
_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), user.ID, pr.Issue.RepoID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, UserIsExplicitlyTrustedWithActions, trust)
|
||||
})
|
||||
|
||||
t.Run("not trusted because on a forked pull request when the user has has no privileges", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // regular user
|
||||
trust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, pr.Issue.PosterID)
|
||||
require.True(t, pr.IsForkPullRequest())
|
||||
assert.Equal(t, UserIsNotTrustedWithActions, trust)
|
||||
})
|
||||
|
||||
t.Run("not trusted on a forked pull request because the user is restricted", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5000})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) // restricted user
|
||||
trust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, pr.Issue.PosterID)
|
||||
require.True(t, pr.IsForkPullRequest())
|
||||
_, err = actions_model.GetActionUserByUserIDAndRepoIDAndUpdateAccess(t.Context(), user.ID, pr.Issue.RepoID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, user.IsRestricted)
|
||||
assert.Equal(t, UserIsNotTrustedWithActions, trust)
|
||||
})
|
||||
|
||||
t.Run("approval not needed because the pr is not from a fork", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2000})
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval not needed because the event is known to run out of the default branch", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequestComment)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval not needed because it is not a pr", func(t *testing.T) {
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), nil, nil, webhook_module.HookEventPullRequestComment)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval not needed for a forked pr because the poster is trusted", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3000})
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, nil, webhook_module.HookEventPullRequestSync)
|
||||
require.NoError(t, err)
|
||||
require.True(t, pr.Issue.Poster.IsAdmin)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval needed for a forked pr because the poster and the doer are not trusted", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // regular user
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestSync)
|
||||
require.NoError(t, err)
|
||||
posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
|
||||
doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsNotTrustedWithActions, doerTrust)
|
||||
assert.Equal(t, actions_model.NeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval not needed for a forked pr because the doer is trusted and runs from the base", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // admin
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestLabel)
|
||||
require.NoError(t, err)
|
||||
posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
|
||||
doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsImplicitlyTrustedWithActions, doerTrust)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useBaseCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("approval not needed for a forked pr because the doer is trusted and pushed new commits", func(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 4000})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // admin
|
||||
useCommit, approval, err := getPullRequestCommitAndApproval(t.Context(), pr, doer, webhook_module.HookEventPullRequestSync)
|
||||
require.NoError(t, err)
|
||||
posterTrust, err := GetPullRequestPosterIsTrustedWithActions(t.Context(), pr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsNotTrustedWithActions, posterTrust)
|
||||
doerTrust, err := getPullRequestUserIsTrustedWithActions(t.Context(), pr, doer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, UserIsImplicitlyTrustedWithActions, doerTrust)
|
||||
assert.Equal(t, actions_model.DoesNotNeedApproval, approval)
|
||||
assert.EqualValues(t, useHeadCommit, useCommit)
|
||||
})
|
||||
|
||||
t.Run("run for a pull request is set with info related to trust", func(t *testing.T) {
|
||||
run := &actions_model.ActionRun{
|
||||
IsForkPullRequest: true,
|
||||
}
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5000})
|
||||
needApproval := actions_model.NeedApproval
|
||||
require.NoError(t, setRunTrustForPullRequest(t.Context(), run, nil, needApproval))
|
||||
require.NoError(t, setRunTrustForPullRequest(t.Context(), run, pr, needApproval))
|
||||
assert.True(t, run.NeedApproval)
|
||||
assert.True(t, run.IsForkPullRequest)
|
||||
assert.Equal(t, pr.Issue.PosterID, run.PullRequestPosterID)
|
||||
assert.Equal(t, pr.ID, run.PullRequestID)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue