From 6ab542607f28d51756c630859064ecb3a07b6d88 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 May 2026 20:56:38 -0700 Subject: [PATCH 1/3] Fix smart http request scope bug --- services/context/permission.go | 6 ++--- tests/integration/git_smart_http_test.go | 33 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/services/context/permission.go b/services/context/permission.go index 1f40e261535ee..a2c207872d0b9 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -57,14 +57,14 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) { } } -// CheckRepoScopedToken check whether personal access token has repo scope +// CheckRepoScopedToken checks whether the authenticated API token has repo scope. func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) { - if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true { + if ctx.Data["IsApiToken"] != true { return } scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { // it's a personal access token but not oauth2 token + if ok { var scopeMatched bool requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository) diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index b74eb528c0874..5bb3d40701915 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -9,6 +9,9 @@ import ( "net/url" "testing" + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" @@ -20,6 +23,7 @@ import ( func TestGitSmartHTTP(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { testGitSmartHTTP(t, u) + testGitSmartHTTPTokenScopes(t) testRenamedRepoRedirect(t) testGitArchiveRemote(t, u) }) @@ -80,6 +84,35 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) { } } +func testGitSmartHTTPTokenScopes(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerName: "user2", Name: "repo2"}) + assert.True(t, repo.IsPrivate) + + session := loginUser(t, "user2") + badToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification) + readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + t.Run("upload-pack requires read repository scope for basic and bearer tokens", func(t *testing.T) { + url := "/user2/repo2/info/refs?service=git-upload-pack" + + MakeRequest(t, NewRequest(t, "GET", url).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(badToken), http.StatusForbidden) + + resp := MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(readToken), http.StatusOK) + assert.Contains(t, resp.Body.String(), "refs/heads/master") + }) + + t.Run("receive-pack requires write repository scope for bearer tokens", func(t *testing.T) { + url := "/user2/repo2/info/refs?service=git-receive-pack" + + MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(readToken), http.StatusForbidden) + + resp := MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(writeToken), http.StatusOK) + assert.Contains(t, resp.Body.String(), "refs/heads/master") + }) +} + func testRenamedRepoRedirect(t *testing.T) { defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() From 8625003ae9527042720e0e0777f78696bae6c37b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 7 May 2026 21:03:08 +0200 Subject: [PATCH 2/3] fix: guard nil repo in CheckRepoScopedToken Smart-HTTP routes call CheckRepoScopedToken before confirming the repository exists, so repo can be nil. Without the guard, a token with the public-only scope would panic on repo.IsPrivate. Co-Authored-By: Claude Opus 4.7 --- services/context/permission.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/context/permission.go b/services/context/permission.go index a2c207872d0b9..16de86bbc62c4 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -76,7 +76,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_ return } - if publicOnly && repo.IsPrivate { + if publicOnly && repo != nil && repo.IsPrivate { ctx.HTTPError(http.StatusForbidden) return } From cb345468543157f3ad897a030ebb47b8283c22d4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 8 May 2026 07:36:25 +0200 Subject: [PATCH 3/3] test: tighten testGitSmartHTTPTokenScopes - assert.True -> require.True for the IsPrivate precondition - rename url to path to avoid shadowing net/url - mirror basic-auth coverage on receive-pack - add subtest for public-only scope rejecting a private repo Co-Authored-By: Claude (Opus 4.7) --- tests/integration/git_smart_http_test.go | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 5bb3d40701915..c535838c8a16a 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -86,31 +86,38 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) { func testGitSmartHTTPTokenScopes(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerName: "user2", Name: "repo2"}) - assert.True(t, repo.IsPrivate) + require.True(t, repo.IsPrivate) session := loginUser(t, "user2") badToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification) readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadRepository) - t.Run("upload-pack requires read repository scope for basic and bearer tokens", func(t *testing.T) { - url := "/user2/repo2/info/refs?service=git-upload-pack" + t.Run("upload-pack requires read repository scope", func(t *testing.T) { + path := "/user2/repo2/info/refs?service=git-upload-pack" - MakeRequest(t, NewRequest(t, "GET", url).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden) - MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(badToken), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(badToken), http.StatusForbidden) - resp := MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(readToken), http.StatusOK) + resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusOK) assert.Contains(t, resp.Body.String(), "refs/heads/master") }) - t.Run("receive-pack requires write repository scope for bearer tokens", func(t *testing.T) { - url := "/user2/repo2/info/refs?service=git-receive-pack" + t.Run("receive-pack requires write repository scope", func(t *testing.T) { + path := "/user2/repo2/info/refs?service=git-receive-pack" - MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(readToken), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(readToken, "x-oauth-basic"), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusForbidden) - resp := MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(writeToken), http.StatusOK) + resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(writeToken), http.StatusOK) assert.Contains(t, resp.Body.String(), "refs/heads/master") }) + + t.Run("public-only scope rejects private repo", func(t *testing.T) { + path := "/user2/repo2/info/refs?service=git-upload-pack" + MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden) + }) } func testRenamedRepoRedirect(t *testing.T) {