fix: allow git clone of private repos with anonymous code access (#38074)

Fixes #38062.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
bircni
2026-06-13 06:02:02 +02:00
committed by GitHub
parent 275dee5bda
commit 9608cc212d
3 changed files with 56 additions and 22 deletions
+22 -21
View File
@@ -58,8 +58,6 @@ func CorsHandler() func(next http.Handler) http.Handler {
// httpBase does the common work for git http services, // httpBase does the common work for git http services,
// including early response, authentication, repository lookup and permission check. // including early response, authentication, repository lookup and permission check.
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if ctx.FormString("go-get") == "1" { if ctx.FormString("go-get") == "1" {
context.EarlyResponseForGoGetMeta(ctx) context.EarlyResponseForGoGetMeta(ctx)
return nil return nil
@@ -93,11 +91,11 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
isWiki := false isWiki := false
unitType := unit.TypeCode unitType := unit.TypeCode
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if strings.HasSuffix(reponame, ".wiki") { if strings.HasSuffix(repoName, ".wiki") {
isWiki = true isWiki = true
unitType = unit.TypeWiki unitType = unit.TypeWiki
reponame = reponame[:len(reponame)-5] repoName = repoName[:len(repoName)-5]
} }
owner := ctx.ContextUser owner := ctx.ContextUser
@@ -107,14 +105,14 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
} }
repoExist := true repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame) repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
if err != nil { if err != nil {
if !repo_model.IsErrRepoNotExist(err) { if !repo_model.IsErrRepoNotExist(err) {
ctx.ServerError("GetRepositoryByName", err) ctx.ServerError("GetRepositoryByName", err)
return nil return nil
} }
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil { if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID) context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil return nil
} }
@@ -127,23 +125,26 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil return nil
} }
// Only public pull don't need auth. // Only public pulls don't need auth: repo must exist, not require-sign-in
isPublicPull := repoExist && !repo.IsPrivate && isPull canAnonymousPull := false
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict if isPull && repoExist && !setting.Service.RequireSignInViewStrict {
// allow anonymous pulls if owner is public and repo is public (not private)
// don't allow anonymous pulls if organization is not public if owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate {
if isPublicPull { canAnonymousPull = true
if err := repo.LoadOwner(ctx); err != nil { }
ctx.ServerError("LoadOwner", err) // then check "public anonymous access" permission
return nil if !canAnonymousPull && ctx.Doer == nil {
anonPerm, err := access_model.GetDoerRepoPermission(ctx, repo, nil)
if err != nil {
ctx.ServerError("GetDoerRepoPermission", err)
return nil
}
canAnonymousPull = anonPerm.CanAccess(accessMode, unitType)
} }
askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
} }
// check access // check access
if askAuth { if !canAnonymousPull { // not public pull, then either the pull needs auth, or the push needs "write" permission, so ask auth
// rely on the results of Contexter
if !ctx.IsSigned { if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit // TODO: support digit auth - which would be Authorization header with digit
if setting.OAuth2.Enabled { if setting.OAuth2.Enabled {
@@ -229,7 +230,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil return nil
} }
repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, reponame) repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, repoName)
if err != nil { if err != nil {
log.Error("pushCreateRepo: %v", err) log.Error("pushCreateRepo: %v", err)
ctx.Status(http.StatusNotFound) ctx.Status(http.StatusNotFound)
+34
View File
@@ -10,7 +10,9 @@ import (
"testing" "testing"
auth_model "gitea.dev/models/auth" auth_model "gitea.dev/models/auth"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/test" "gitea.dev/modules/test"
@@ -26,6 +28,8 @@ func TestGitSmartHTTP(t *testing.T) {
testGitSmartHTTPTokenScopes(t) testGitSmartHTTPTokenScopes(t)
testRenamedRepoRedirect(t) testRenamedRepoRedirect(t)
testGitArchiveRemote(t, u) testGitArchiveRemote(t, u)
t.Run("AnonymousAccess-Repo", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, false) })
t.Run("AnonymousAccess-Wiki", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, true) })
}) })
} }
@@ -144,3 +148,33 @@ func testGitArchiveRemote(t *testing.T, u *url.URL) {
t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test")) t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test"))
t.Run("list compression options", doGitRemoteArchive(u.String(), "--list")) t.Run("list compression options", doGitRemoteArchive(u.String(), "--list"))
} }
// testGitSmartHTTPPrivateRepoAnonymousAccess tests that a private repo with
// anonymous code access enabled can be cloned without credentials.
func testGitSmartHTTPPrivateRepoAnonymousAccess(t *testing.T, isWiki bool) {
// repo1 (ID=1) belongs to user2 and is public by default in fixtures
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerName: "user2", Name: "repo1"})
unitType := util.Iif(isWiki, unit.TypeWiki, unit.TypeCode)
repoLink := "/" + repo.FullName() + util.Iif(isWiki, ".wiki", "")
gitPullPath := repoLink + "/info/refs?service=git-upload-pack"
gitPushPath := repoLink + "/info/refs?service=git-receive-pack"
// make the repo private
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: repo.ID, IsPrivate: true}, "is_private"))
// without anonymous access: anonymous pull must require auth
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized)
// enable anonymous read access on the unit
require.NoError(t, repo_model.UpdateRepoUnitPublicAccess(t.Context(), &repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, AnonymousAccessMode: perm.AccessModeRead}))
// with anonymous code access: anonymous pull must succeed without credentials
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusOK)
// push (receive-pack) must still require auth even with anonymous code access
MakeRequest(t, NewRequest(t, "GET", gitPushPath), http.StatusUnauthorized)
// RequireSignInViewStrict must override anonymous access
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized)
}
-1
View File
@@ -5,7 +5,6 @@ RUN_MODE = prod
[database] [database]
DB_TYPE = sqlite3 DB_TYPE = sqlite3
PATH = gitea-test.db PATH = gitea-test.db
SQLITE_JOURNAL_MODE = WAL
[indexer] [indexer]
REPO_INDEXER_ENABLED = true REPO_INDEXER_ENABLED = true