- 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
327 lines
9.6 KiB
Go
327 lines
9.6 KiB
Go
// 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
|
|
}
|