mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-14 03:29:55 +00:00
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:
+22
-21
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user