Skip to content

Commit c5b32f0

Browse files
chmouelclaude
authored andcommitted
fix: Check source project access for GitLab PRs
- Added a check to ensure the GitLab token has read access to the source repository when processing pull requests. - This addresses an issue where builds could fail if the token lacked the necessary scope to access private source repositories. https://issues.redhat.com/browse/SRVKP-8902 Co-Authored-By: Claude <[email protected]> Signed-off-by: Chmouel Boudjnah <[email protected]>
1 parent 49df439 commit c5b32f0

File tree

3 files changed

+134
-18
lines changed

3 files changed

+134
-18
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/google/cel-go v0.26.1
1717
github.com/google/go-cmp v0.7.0
1818
github.com/google/go-github/scrape v0.0.0-20250818135035-f137c94931a7
19+
github.com/google/go-github/v72 v72.0.0
1920
github.com/google/go-github/v74 v74.0.0
2021
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
2122
github.com/jenkins-x/go-scm v1.15.16
@@ -69,7 +70,6 @@ require (
6970
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
7071
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
7172
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
72-
github.com/google/go-github/v72 v72.0.0 // indirect
7373
github.com/google/gofuzz v1.2.0 // indirect
7474
github.com/rickb777/plural v1.4.4 // indirect
7575
github.com/robfig/cron/v3 v3.0.1 // indirect

pkg/provider/gitlab/gitlab.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,11 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, runevent *info.
195195
}
196196
v.apiURL = apiURL
197197

198-
v.gitlabClient, err = gitlab.NewClient(runevent.Provider.Token, gitlab.WithBaseURL(apiURL))
199-
if err != nil {
200-
return err
198+
if v.gitlabClient == nil {
199+
v.gitlabClient, err = gitlab.NewClient(runevent.Provider.Token, gitlab.WithBaseURL(apiURL))
200+
if err != nil {
201+
return err
202+
}
201203
}
202204
v.Token = &runevent.Provider.Token
203205

@@ -211,6 +213,19 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, runevent *info.
211213
v.sourceProjectID = runevent.SourceProjectID
212214
}
213215

216+
// check that we have access to the source project if it's a private repo, this should only occur on Merge Requests
217+
if runevent.TriggerTarget == triggertype.PullRequest {
218+
_, resp, err := v.Client().Projects.GetProject(runevent.SourceProjectID, &gitlab.GetProjectOptions{})
219+
errmsg := fmt.Sprintf("failed to access GitLab source repository ID %d: please ensure token has 'read_repository' scope on that repository",
220+
runevent.SourceProjectID)
221+
if resp != nil && resp.StatusCode == http.StatusNotFound {
222+
return fmt.Errorf("%s", errmsg)
223+
}
224+
if err != nil {
225+
return fmt.Errorf("%s: %w", errmsg, err)
226+
}
227+
}
228+
214229
// if we don't have sourceProjectID (ie: incoming-webhook) then try to set
215230
// it ASAP if we can.
216231
if v.sourceProjectID == 0 && runevent.Organization != "" && runevent.Repository != "" {
@@ -280,27 +295,35 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusOpts
280295
Context: gitlab.Ptr(contextName),
281296
}
282297

283-
// setCommitStatus attempts to set the commit status for a given SHA, handling GitLab's permission model.
284-
// First tries the source project (fork), then falls back to the target project (upstream repo).
285-
// This fallback is necessary because:
286-
// - In fork/MR scenarios, the GitLab token may not have write access to the contributor's fork
287-
// - This ensures commit status can be displayed somewhere useful regardless
288-
// of permission differences
289-
// Returns nil if status is set on either project, logs error if both attempts fail.
298+
// In case we have access, set the status. Typically, on a Merge Request (MR)
299+
// from a fork in an upstream repository, the token needs to have write access
300+
// to the fork repository in order to create a status. However, the token set on the
301+
// Repository CR usually doesn't have such broad access, preventing from creating
302+
// a status comment on it.
303+
// This would work on a push or an MR from a branch within the same repo.
304+
// Ignoring errors because of the write access issues,
290305
_, _, err := v.Client().Commits.SetCommitStatus(event.SourceProjectID, event.SHA, opt)
291-
if err == nil {
292-
v.Logger.Debugf("created commit status on source project ID %d", event.SourceProjectID)
306+
if err != nil {
307+
v.Logger.Debugf("cannot set status with the GitLab token on the source project: %v", err)
308+
} else {
309+
// we managed to set the status on the source repo, all good we are done
310+
v.Logger.Debugf("created commit status on source project ID %d", event.TargetProjectID)
293311
return nil
294312
}
295313
if _, _, err2 := v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt); err2 == nil {
296314
v.Logger.Debugf("created commit status on target project ID %d", event.TargetProjectID)
315+
// we managed to set the status on the target repo, all good we are done
297316
return nil
298317
}
299-
v.Logger.Debugf(
300-
"Failed to create commit status: source project ID %d, target project ID %d. "+
301-
"If you want Gitlab Pipeline Status update, ensure your GitLab token has access "+
302-
"to the source repository. Error: %v",
303-
event.SourceProjectID, event.TargetProjectID, err)
318+
v.Logger.Debugf("cannot set status with the GitLab token on the target project: %v", err)
319+
// we only show the first error as it's likely something the user has more control to fix
320+
// the second err is cryptic as it needs a dummy gitlab pipeline to start
321+
// with and will only give more confusion in the event namespace
322+
v.eventEmitter.EmitMessage(v.repo, zap.InfoLevel, "FailedToSetCommitStatus",
323+
fmt.Sprintf("failed to create commit status: source project ID %d, target project ID %d. "+
324+
"If you want Gitlab Pipeline Status update, ensure your GitLab token giving it access "+
325+
"to the source repository. %v",
326+
event.SourceProjectID, event.TargetProjectID, err))
304327

305328
eventType := triggertype.IsPullRequestType(event.EventType)
306329
// When a GitOps command is sent on a pushed commit, it mistakenly treats it as a pull_request

pkg/provider/gitlab/gitlab_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,99 @@ func TestSetClient(t *testing.T) {
396396
assert.Equal(t, expected, logs[0].Message)
397397
}
398398

399+
func TestSetClientRepositoryAccessCheck(t *testing.T) {
400+
ctx, _ := rtesting.SetupFakeContext(t)
401+
observer, _ := zapobserver.New(zap.InfoLevel)
402+
fakelogger := zap.New(observer).Sugar()
403+
run := &params.Run{
404+
Clients: clients.Clients{
405+
Log: fakelogger,
406+
},
407+
}
408+
409+
tests := []struct {
410+
name string
411+
triggerTarget triggertype.Trigger
412+
sourceProjectID int
413+
setupMockResponse func(*http.ServeMux, int)
414+
expectedError string
415+
}{
416+
{
417+
name: "Non-pull request trigger should skip access check",
418+
triggerTarget: triggertype.Push,
419+
sourceProjectID: 123,
420+
setupMockResponse: func(_ *http.ServeMux, _ int) {
421+
// No mock needed - should not make the call
422+
},
423+
expectedError: "",
424+
},
425+
{
426+
name: "Pull request with successful access",
427+
triggerTarget: triggertype.PullRequest,
428+
sourceProjectID: 123,
429+
setupMockResponse: func(mux *http.ServeMux, projectID int) {
430+
path := fmt.Sprintf("/projects/%d", projectID)
431+
mux.HandleFunc(path, func(rw http.ResponseWriter, r *http.Request) {
432+
if r.Method == http.MethodGet {
433+
rw.WriteHeader(http.StatusOK)
434+
fmt.Fprint(rw, `{"id": 123, "name": "test-repo"}`)
435+
}
436+
})
437+
},
438+
expectedError: "",
439+
},
440+
{
441+
name: "Pull request with not found should return specific error",
442+
triggerTarget: triggertype.PullRequest,
443+
sourceProjectID: 456,
444+
setupMockResponse: func(mux *http.ServeMux, projectID int) {
445+
path := fmt.Sprintf("/projects/%d", projectID)
446+
mux.HandleFunc(path, func(rw http.ResponseWriter, r *http.Request) {
447+
if r.Method == http.MethodGet {
448+
// Return 404 without error to test the status code check
449+
rw.WriteHeader(http.StatusNotFound)
450+
fmt.Fprint(rw, `{"message": "404 Project Not Found"}`)
451+
}
452+
})
453+
},
454+
expectedError: "failed to access GitLab source repository ID 456: please ensure token has 'read_repository' scope on that repository",
455+
},
456+
}
457+
458+
for _, tt := range tests {
459+
t.Run(tt.name, func(t *testing.T) {
460+
mockClient, mux, tearDown := thelp.Setup(t)
461+
defer tearDown()
462+
463+
// Setup the mock for the repository access check API call
464+
if tt.setupMockResponse != nil {
465+
tt.setupMockResponse(mux, tt.sourceProjectID)
466+
}
467+
468+
v := &Provider{gitlabClient: mockClient}
469+
event := &info.Event{
470+
Provider: &info.Provider{
471+
Token: "test-token",
472+
},
473+
Organization: "test-org",
474+
Repository: "test-repo",
475+
TriggerTarget: tt.triggerTarget,
476+
SourceProjectID: tt.sourceProjectID,
477+
TargetProjectID: 123,
478+
}
479+
480+
err := v.SetClient(ctx, run, event, nil, nil)
481+
482+
if tt.expectedError != "" {
483+
assert.Assert(t, err != nil, "expected error but got none")
484+
assert.ErrorContains(t, err, tt.expectedError)
485+
} else {
486+
assert.NilError(t, err, "unexpected error: %v", err)
487+
}
488+
})
489+
}
490+
}
491+
399492
func TestSetClientDetectAPIURL(t *testing.T) {
400493
ctx, _ := rtesting.SetupFakeContext(t)
401494
observer, _ := zapobserver.New(zap.InfoLevel)

0 commit comments

Comments
 (0)