Skip to content

Commit a6e4aab

Browse files
chmouelCursor
andcommitted
fix restrict same repo ACL perm to trusted context
Avoid granting issue_comment senders trust based solely on same-repo PR shape. Keep the same-repo fast path for pull_request events and PR rerequest flows, while forcing comment senders through collaborator, org-membership, or OWNERS checks. Add regression coverage to ensure issue_comment events do not bypass ACL and same-repo check_run/check_suite rerequests continue to work. Adjust the GitHub ok-to-test e2e to assert the untrusted comment path without waiting for MinNumberStatus=1. That wait only revalidated the original pull request status and did not prove that the issue_comment event triggered anything. The updated test now verifies that an untrusted same-repo issue_comment does not create a new PipelineRun or Repository status and emits the expected debug log. Fixes #2664 Co-authored-by: Cursor <cursor@users.noreply.github.com> Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent de6de63 commit a6e4aab

File tree

3 files changed

+182
-56
lines changed

3 files changed

+182
-56
lines changed

pkg/provider/github/acl.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,18 @@ func (v *Provider) aclCheckAll(ctx context.Context, rev *info.Event) (bool, erro
204204
return true, nil
205205
}
206206

207-
// If the user who has submitted the PR is not a owner or public member or Collaborator or not there in OWNERS file
208-
// but has permission to push to branches then allow the CI to be run.
209-
// This can only happen with GithubApp and Bots.
210-
// Ex: dependabot, bots
211-
if rev.PullRequestNumber != 0 {
207+
// Allow same-repo pull requests from bots or other non-members when the PR
208+
// branch lives in this repository instead of a fork.
209+
if v.canUseSameRepoPullRequestShortcut(rev) {
212210
isFromSameRepo := v.checkPullRequestForSameURL(ctx, rev)
213211
if isFromSameRepo {
214212
return true, nil
215213
}
214+
} else if rev.PullRequestNumber != 0 && v.Logger != nil {
215+
v.Logger.Debugf(
216+
"Skipping same-repo pull request shortcut for untrusted event %T on %s/%s#%d from sender %s",
217+
rev.Event, rev.Organization, rev.Repository, rev.PullRequestNumber, rev.Sender,
218+
)
216219
}
217220

218221
// If the user who has submitted the pr is a owner on the repo then allows
@@ -238,13 +241,30 @@ func (v *Provider) aclCheckAll(ctx context.Context, rev *info.Event) (bool, erro
238241
return v.IsAllowedOwnersFile(ctx, rev)
239242
}
240243

241-
// checkPullRequestForSameURL checks if a pull request's head and base branches are from the same repository.
242-
// means if the user has access to create a branch in the repository without forking or having any
243-
// permissions then PAC should allow to run CI.
244+
// canUseSameRepoPullRequestShortcut returns true only for event types where
245+
// the sender is expected to match the pull request author. That keeps the
246+
// same-repo shortcut available for PR and rerequest flows, but not comments.
247+
func (v *Provider) canUseSameRepoPullRequestShortcut(rev *info.Event) bool {
248+
if rev.PullRequestNumber == 0 {
249+
return false
250+
}
251+
252+
switch rev.Event.(type) {
253+
case *github.PullRequestEvent, *github.CheckRunEvent, *github.CheckSuiteEvent:
254+
return true
255+
default:
256+
return false
257+
}
258+
}
259+
260+
// checkPullRequestForSameURL returns true when the PR comes from another branch
261+
// in the same repository instead of from a fork.
244262
//
245-
// ex: dependabot, *[bot] etc...
263+
// This is the fast path that lets bot-authored PRs such as Dependabot run
264+
// without needing collaborator, org-member, or OWNERS access.
246265
//
247-
// HeadURL is set by getPullRequest() before aclCheckAll; if missing, fall through to other ACL checks.
266+
// HeadURL is filled by getPullRequest() before aclCheckAll. If it is missing,
267+
// ACL falls back to the regular membership checks.
248268
func (v *Provider) checkPullRequestForSameURL(_ context.Context, runevent *info.Event) bool {
249269
if runevent.HeadURL == "" {
250270
return false

pkg/provider/github/acl_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ func TestIfPullRequestIsForSameRepoWithoutFork(t *testing.T) {
708708
BaseURL: "http://org.com/owner/repo",
709709
HeadBranch: "main",
710710
BaseBranch: "dependabot",
711+
Event: &github.PullRequestEvent{},
711712
},
712713
pullRequestNumber: 1,
713714
allowed: true,
@@ -719,6 +720,7 @@ func TestIfPullRequestIsForSameRepoWithoutFork(t *testing.T) {
719720
Sender: "nonowner",
720721
Repository: "repo",
721722
PullRequestNumber: 1,
723+
Event: &github.PullRequestEvent{},
722724
},
723725
pullRequestNumber: 1,
724726
allowed: false,
@@ -734,10 +736,59 @@ func TestIfPullRequestIsForSameRepoWithoutFork(t *testing.T) {
734736
BaseURL: "http://org.com/owner/repo1",
735737
HeadBranch: "main",
736738
BaseBranch: "dependabot",
739+
Event: &github.PullRequestEvent{},
737740
},
738741
pullRequestNumber: 1,
739742
allowed: false,
740743
wantError: false,
744+
}, {
745+
name: "when issue comment sender is not trusted, same repo shortcut is not applied",
746+
event: &info.Event{
747+
Organization: "owner",
748+
Sender: "nonowner",
749+
Repository: "repo",
750+
PullRequestNumber: 1,
751+
HeadURL: "http://org.com/owner/repo",
752+
BaseURL: "http://org.com/owner/repo",
753+
HeadBranch: "main",
754+
BaseBranch: "dependabot",
755+
Event: &github.IssueCommentEvent{},
756+
},
757+
pullRequestNumber: 1,
758+
allowed: false,
759+
wantError: false,
760+
}, {
761+
name: "when check run rerequest resolves to same repo pull request the shortcut is applied",
762+
event: &info.Event{
763+
Organization: "owner",
764+
Sender: "dependabot[bot]",
765+
Repository: "repo",
766+
PullRequestNumber: 1,
767+
HeadURL: "http://org.com/owner/repo",
768+
BaseURL: "http://org.com/owner/repo",
769+
HeadBranch: "dependabot/npm-foo",
770+
BaseBranch: "main",
771+
Event: &github.CheckRunEvent{},
772+
},
773+
pullRequestNumber: 1,
774+
allowed: true,
775+
wantError: false,
776+
}, {
777+
name: "when check suite rerequest resolves to same repo pull request the shortcut is applied",
778+
event: &info.Event{
779+
Organization: "owner",
780+
Sender: "dependabot[bot]",
781+
Repository: "repo",
782+
PullRequestNumber: 1,
783+
HeadURL: "http://org.com/owner/repo",
784+
BaseURL: "http://org.com/owner/repo",
785+
HeadBranch: "dependabot/npm-foo",
786+
BaseBranch: "main",
787+
Event: &github.CheckSuiteEvent{},
788+
},
789+
pullRequestNumber: 1,
790+
allowed: true,
791+
wantError: false,
741792
},
742793
}
743794
for _, tt := range tests {
@@ -765,3 +816,35 @@ func TestIfPullRequestIsForSameRepoWithoutFork(t *testing.T) {
765816
})
766817
}
767818
}
819+
820+
func TestACLCheckAllIssueCommentLogsShortcutSkip(t *testing.T) {
821+
fakeclient, _, _, teardown := ghtesthelper.SetupGH()
822+
defer teardown()
823+
824+
ctx, _ := rtesting.SetupFakeContext(t)
825+
repo := &v1alpha1.Repository{Spec: v1alpha1.RepositorySpec{
826+
Settings: &v1alpha1.Settings{},
827+
}}
828+
observer, logs := zapobserver.New(zap.DebugLevel)
829+
830+
gprovider := Provider{
831+
ghClient: fakeclient,
832+
repo: repo,
833+
Logger: zap.New(observer).Sugar(),
834+
}
835+
836+
allowed, err := gprovider.aclCheckAll(ctx, &info.Event{
837+
Organization: "owner",
838+
Sender: "nonowner",
839+
Repository: "repo",
840+
PullRequestNumber: 1,
841+
HeadURL: "http://org.com/owner/repo",
842+
BaseURL: "http://org.com/owner/repo",
843+
HeadBranch: "main",
844+
BaseBranch: "dependabot",
845+
Event: &github.IssueCommentEvent{},
846+
})
847+
assert.NilError(t, err)
848+
assert.Equal(t, false, allowed)
849+
assert.Assert(t, logs.FilterMessageSnippet("Skipping same-repo pull request shortcut for untrusted event").Len() == 1)
850+
}

test/github_pullrequest_oktotest_test.go

Lines changed: 69 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import (
77
"fmt"
88
"net/http"
99
"os"
10+
"regexp"
1011
"strconv"
1112
"testing"
13+
"time"
1214

1315
"github.com/google/go-github/v84/github"
16+
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
1417
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
18+
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/cctx"
1519
tgithub "github.com/openshift-pipelines/pipelines-as-code/test/pkg/github"
1620
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/payload"
1721
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
1822
"gotest.tools/v3/assert"
19-
corev1 "k8s.io/api/core/v1"
2023
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2124
)
2225

@@ -41,60 +44,80 @@ func TestGithubGHEPullRequestOkToTest(t *testing.T) {
4144
Organization: g.Options.Organization,
4245
Repository: g.Options.Repo,
4346
URL: repoinfo.GetHTMLURL(),
44-
Sender: g.Options.Organization,
4547
}
4648

47-
installID, err := strconv.ParseInt(os.Getenv("TEST_GITHUB_SECOND_APPLICATION_ID"), 10, 64)
49+
repo, err := g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
50+
assert.NilError(t, err)
51+
initialStatusCount := len(repo.Status)
52+
53+
pruns, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
54+
LabelSelector: fmt.Sprintf("%s=%s", keys.SHA, g.SHA),
55+
})
56+
assert.NilError(t, err)
57+
initialPipelineRunCount := len(pruns.Items)
58+
59+
installID, err := strconv.ParseInt(os.Getenv("TEST_GITHUB_SECOND_REPO_INSTALLATION_ID"), 10, 64)
4860
assert.NilError(t, err)
49-
event := github.IssueCommentEvent{
50-
Comment: &github.IssueComment{
51-
Body: github.Ptr(`/ok-to-test`),
52-
},
53-
Installation: &github.Installation{
54-
ID: &installID,
55-
},
56-
Action: github.Ptr("created"),
57-
Issue: &github.Issue{
58-
State: github.Ptr("open"),
59-
PullRequestLinks: &github.PullRequestLinks{
60-
HTMLURL: github.Ptr(fmt.Sprintf("%s/pull/%d", runevent.URL, g.PRNumber)),
61+
62+
sendIssueComment := func(t *testing.T, sender string) {
63+
t.Helper()
64+
65+
event := github.IssueCommentEvent{
66+
Comment: &github.IssueComment{
67+
Body: github.Ptr(`/ok-to-test`),
68+
},
69+
Installation: &github.Installation{
70+
ID: &installID,
71+
},
72+
Action: github.Ptr("created"),
73+
Issue: &github.Issue{
74+
State: github.Ptr("open"),
75+
PullRequestLinks: &github.PullRequestLinks{
76+
HTMLURL: github.Ptr(fmt.Sprintf("%s/pull/%d", runevent.URL, g.PRNumber)),
77+
},
6178
},
62-
},
63-
Repo: &github.Repository{
64-
DefaultBranch: &runevent.DefaultBranch,
65-
HTMLURL: &runevent.URL,
66-
Name: &runevent.Repository,
67-
Owner: &github.User{Login: &runevent.Organization},
68-
},
69-
Sender: &github.User{
70-
Login: &runevent.Sender,
71-
},
79+
Repo: &github.Repository{
80+
DefaultBranch: &runevent.DefaultBranch,
81+
HTMLURL: &runevent.URL,
82+
Name: &runevent.Repository,
83+
Owner: &github.User{Login: &runevent.Organization},
84+
},
85+
Sender: &github.User{
86+
Login: github.Ptr(sender),
87+
},
88+
}
89+
90+
err = payload.Send(ctx,
91+
g.Cnx,
92+
os.Getenv("TEST_GITHUB_SECOND_EL_URL"),
93+
os.Getenv("TEST_GITHUB_SECOND_WEBHOOK_SECRET"),
94+
os.Getenv("TEST_GITHUB_SECOND_API_URL"),
95+
os.Getenv("TEST_GITHUB_SECOND_REPO_INSTALLATION_ID"),
96+
event,
97+
"issue_comment",
98+
)
99+
assert.NilError(t, err)
72100
}
73101

74-
err = payload.Send(ctx,
75-
g.Cnx,
76-
os.Getenv("TEST_GITHUB_SECOND_EL_URL"),
77-
os.Getenv("TEST_GITHUB_SECOND_WEBHOOK_SECRET"),
78-
os.Getenv("TEST_GITHUB_SECOND_API_URL"),
79-
os.Getenv("TEST_GITHUB_SECOND_APPLICATION_ID"),
80-
event,
81-
"issue_comment",
82-
)
102+
g.Cnx.Clients.Log.Infof("Sending /ok-to-test from untrusted sender on same-repo pull request")
103+
sendIssueComment(t, "nonowner")
104+
105+
time.Sleep(10 * time.Second)
106+
107+
pruns, err = g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{
108+
LabelSelector: fmt.Sprintf("%s=%s", keys.SHA, g.SHA),
109+
})
83110
assert.NilError(t, err)
111+
assert.Equal(t, initialPipelineRunCount, len(pruns.Items), "untrusted issue_comment must not create a new PipelineRun")
84112

85-
g.Cnx.Clients.Log.Infof("Wait for the second repository update to be updated")
86-
waitOpts := twait.Opts{
87-
RepoName: g.TargetNamespace,
88-
Namespace: g.TargetNamespace,
89-
MinNumberStatus: 1,
90-
PollTimeout: twait.DefaultTimeout,
91-
TargetSHA: g.SHA,
92-
}
93-
_, err = twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts)
113+
repo, err = g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
94114
assert.NilError(t, err)
115+
assert.Equal(t, initialStatusCount, len(repo.Status), "untrusted issue_comment must not add a new Repository status")
95116

96-
g.Cnx.Clients.Log.Infof("Check if we have the repository set as succeeded")
97-
repo, err := g.Cnx.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(g.TargetNamespace).Get(ctx, g.TargetNamespace, metav1.GetOptions{})
117+
ctx, err = cctx.GetControllerCtxInfo(ctx, g.Cnx)
118+
assert.NilError(t, err)
119+
numLines := int64(1000)
120+
logRegex := regexp.MustCompile(`Skipping same-repo pull request shortcut for untrusted event \*github\.IssueCommentEvent`)
121+
err = twait.RegexpMatchingInControllerLog(ctx, g.Cnx, *logRegex, 10, "ghe-controller", &numLines, nil)
98122
assert.NilError(t, err)
99-
assert.Assert(t, repo.Status[len(repo.Status)-1].Conditions[0].Status == corev1.ConditionTrue)
100123
}

0 commit comments

Comments
 (0)