From 0e50c6c2900f3fce6afb4fc4f3f913a30e11e0fe Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Mar 2026 14:58:45 +0100 Subject: [PATCH 1/5] Allow token auth for actions badge SVG endpoint Enable OAuth2 and basic auth token processing for the actions workflow badge path so that badges can be retrieved for private repositories. Co-Authored-By: Claude Opus 4.6 --- services/auth/auth.go | 6 ++++++ services/auth/auth_test.go | 5 +++++ services/auth/basic.go | 2 +- services/auth/oauth2.go | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 90e2115bc5f60..75847fb2958ce 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -28,6 +28,7 @@ type globalVarsStruct struct { archivePathRe *regexp.Regexp feedPathRe *regexp.Regexp feedRefPathRe *regexp.Regexp + badgePathRe *regexp.Regexp } var globalVars = sync.OnceValue(func() *globalVarsStruct { @@ -37,6 +38,7 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." + badgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), // "/owner/repo/actions/workflows/foo/badge.svg" } }) @@ -112,6 +114,10 @@ func (a *authPathDetector) isArchivePath() bool { return a.vars.archivePathRe.MatchString(a.req.URL.Path) } +func (a *authPathDetector) isBadgePath() bool { + return a.vars.badgePathRe.MatchString(a.req.URL.Path) +} + func (a *authPathDetector) isAuthenticatedTokenRequest() bool { switch a.req.URL.Path { case "/login/oauth/userinfo", "/login/oauth/introspect": diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go index c45f312c90f8b..94a9a9b73073a 100644 --- a/services/auth/auth_test.go +++ b/services/auth/auth_test.go @@ -131,6 +131,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) { } } +func Test_isBadgePath(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "http://localhost/owner/repo/actions/workflows/build.yml/badge.svg", nil) + assert.True(t, newAuthPathDetector(req).isBadgePath()) +} + func Test_isFeedRequest(t *testing.T) { tests := []struct { want bool diff --git a/services/auth/basic.go b/services/auth/basic.go index 3161d7f33d4c2..cd1acdbf0845b 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -44,7 +44,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, // Basic authentication should only fire on API, Feed, Download, Archives or on Git or LFSPaths // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() { + if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() && !detector.isBadgePath() { return ret } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 86903b0ce1715..de20425590b95 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -155,7 +155,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs detector := newAuthPathDetector(req) if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && - !detector.isGitRawOrAttachPath() && !detector.isArchivePath() { + !detector.isGitRawOrAttachPath() && !detector.isArchivePath() && !detector.isBadgePath() { return nil, nil //nolint:nilnil // the auth method is not applicable } From 902f4df0ad8047eedb2a9c13b3d996418b16b129 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Mar 2026 15:08:46 +0100 Subject: [PATCH 2/5] fmt --- services/auth/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 75847fb2958ce..6c48e63bd9759 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -36,8 +36,8 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`), archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), - feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" - feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." + feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" + feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." badgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), // "/owner/repo/actions/workflows/foo/badge.svg" } }) From b0d4f174cc67afbb478741a700cdee79209b8b6b Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Mar 2026 15:10:13 +0100 Subject: [PATCH 3/5] reformat comments --- services/auth/auth.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 6c48e63bd9759..3c9307e1e9126 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -36,9 +36,12 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`), archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), - feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" - feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." - badgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), // "/owner/repo/actions/workflows/foo/badge.svg" + // "/owner.rss" or "/owner/repo.atom" + feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), + // "/owner/repo/rss/branch/..." + feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), + // "/owner/repo/actions/workflows/foo/badge.svg" + badgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), } }) From a322d2d0058ae253662620214ff59d477491d029 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Mar 2026 06:28:58 +0100 Subject: [PATCH 4/5] rename badge vars to actionsBadge for clarity --- services/auth/auth.go | 8 ++++---- services/auth/auth_test.go | 4 ++-- services/auth/basic.go | 2 +- services/auth/oauth2.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index 3c9307e1e9126..f01fe12d8c2a2 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -28,7 +28,7 @@ type globalVarsStruct struct { archivePathRe *regexp.Regexp feedPathRe *regexp.Regexp feedRefPathRe *regexp.Regexp - badgePathRe *regexp.Regexp + actionsBadgePathRe *regexp.Regexp } var globalVars = sync.OnceValue(func() *globalVarsStruct { @@ -41,7 +41,7 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { // "/owner/repo/rss/branch/..." feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/actions/workflows/foo/badge.svg" - badgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), + actionsBadgePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/actions/workflows/[-.\w]+/badge\.svg$`), } }) @@ -117,8 +117,8 @@ func (a *authPathDetector) isArchivePath() bool { return a.vars.archivePathRe.MatchString(a.req.URL.Path) } -func (a *authPathDetector) isBadgePath() bool { - return a.vars.badgePathRe.MatchString(a.req.URL.Path) +func (a *authPathDetector) isActionsBadgePath() bool { + return a.vars.actionsBadgePathRe.MatchString(a.req.URL.Path) } func (a *authPathDetector) isAuthenticatedTokenRequest() bool { diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go index 94a9a9b73073a..558fecc737fdc 100644 --- a/services/auth/auth_test.go +++ b/services/auth/auth_test.go @@ -131,9 +131,9 @@ func Test_isGitRawOrLFSPath(t *testing.T) { } } -func Test_isBadgePath(t *testing.T) { +func Test_isActionsBadgePath(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "http://localhost/owner/repo/actions/workflows/build.yml/badge.svg", nil) - assert.True(t, newAuthPathDetector(req).isBadgePath()) + assert.True(t, newAuthPathDetector(req).isActionsBadgePath()) } func Test_isFeedRequest(t *testing.T) { diff --git a/services/auth/basic.go b/services/auth/basic.go index cd1acdbf0845b..19e8f72d1792b 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -44,7 +44,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, // Basic authentication should only fire on API, Feed, Download, Archives or on Git or LFSPaths // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() && !detector.isBadgePath() { + if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() && !detector.isActionsBadgePath() { return ret } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index de20425590b95..1377d4724ffd0 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -155,7 +155,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs detector := newAuthPathDetector(req) if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && - !detector.isGitRawOrAttachPath() && !detector.isArchivePath() && !detector.isBadgePath() { + !detector.isGitRawOrAttachPath() && !detector.isArchivePath() && !detector.isActionsBadgePath() { return nil, nil //nolint:nilnil // the auth method is not applicable } From 128279dae5a6c345cf799f2519492afb7f739011 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Mar 2026 06:31:17 +0100 Subject: [PATCH 5/5] restrict actions badge auth to GET, rename to isActionsBadgeRequest --- services/auth/auth.go | 4 ++-- services/auth/auth_test.go | 4 ++-- services/auth/basic.go | 2 +- services/auth/oauth2.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index f01fe12d8c2a2..305746eaf8533 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -117,8 +117,8 @@ func (a *authPathDetector) isArchivePath() bool { return a.vars.archivePathRe.MatchString(a.req.URL.Path) } -func (a *authPathDetector) isActionsBadgePath() bool { - return a.vars.actionsBadgePathRe.MatchString(a.req.URL.Path) +func (a *authPathDetector) isActionsBadgeRequest() bool { + return a.req.Method == http.MethodGet && a.vars.actionsBadgePathRe.MatchString(a.req.URL.Path) } func (a *authPathDetector) isAuthenticatedTokenRequest() bool { diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go index 558fecc737fdc..9035fd5d5f466 100644 --- a/services/auth/auth_test.go +++ b/services/auth/auth_test.go @@ -131,9 +131,9 @@ func Test_isGitRawOrLFSPath(t *testing.T) { } } -func Test_isActionsBadgePath(t *testing.T) { +func Test_isActionsBadgeRequest(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "http://localhost/owner/repo/actions/workflows/build.yml/badge.svg", nil) - assert.True(t, newAuthPathDetector(req).isActionsBadgePath()) + assert.True(t, newAuthPathDetector(req).isActionsBadgeRequest()) } func Test_isFeedRequest(t *testing.T) { diff --git a/services/auth/basic.go b/services/auth/basic.go index 19e8f72d1792b..0c7bacc7e0531 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -44,7 +44,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, // Basic authentication should only fire on API, Feed, Download, Archives or on Git or LFSPaths // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() && !detector.isActionsBadgePath() { + if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() && !detector.isActionsBadgeRequest() { return ret } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 1377d4724ffd0..17fd8745e3bd4 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -155,7 +155,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs detector := newAuthPathDetector(req) if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && - !detector.isGitRawOrAttachPath() && !detector.isArchivePath() && !detector.isActionsBadgePath() { + !detector.isGitRawOrAttachPath() && !detector.isArchivePath() && !detector.isActionsBadgeRequest() { return nil, nil //nolint:nilnil // the auth method is not applicable }