0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-03 14:43:03 +02:00

Merge 34a36ef51af3677c626878391413341325064770 into 6eed75af248ae597d854a3c5e6b8831a5ff76290

This commit is contained in:
Lunny Xiao 2026-04-03 04:39:47 +00:00 committed by GitHub
commit 759e05b272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 28 deletions

View File

@ -38,6 +38,7 @@
base_branch: master
merge_base: 0abcb056019adb83
has_merged: false
allow_maintainer_edit: true
-
id: 4

View File

@ -64,7 +64,7 @@ func GetListLockHandler(ctx *context.Context) {
return
}
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
authenticated := authenticate(ctx, repository, rv.Authorization, true, false, "")
if !authenticated {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
@ -153,7 +153,7 @@ func PostLockHandler(ctx *context.Context) {
return
}
authenticated := authenticate(ctx, repository, authorization, true, true)
authenticated := authenticate(ctx, repository, authorization, true, true, "")
if !authenticated {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
@ -218,7 +218,7 @@ func VerifyLockHandler(ctx *context.Context) {
return
}
authenticated := authenticate(ctx, repository, authorization, true, true)
authenticated := authenticate(ctx, repository, authorization, true, true, "")
if !authenticated {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
@ -286,7 +286,7 @@ func UnLockHandler(ctx *context.Context) {
return
}
authenticated := authenticate(ctx, repository, authorization, true, true)
authenticated := authenticate(ctx, repository, authorization, true, true, "")
if !authenticated {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{

View File

@ -20,12 +20,14 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/httpauth"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
lfs_module "code.gitea.io/gitea/modules/lfs"
@ -45,6 +47,10 @@ type requestContext struct {
RepoGitURL string
}
const (
lfsRefQueryKey = "lfs-ref"
)
// Claims is a JWT Token Claims
type Claims struct {
RepoID int64
@ -85,14 +91,22 @@ func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid)
}
func appendRefQuery(baseURL, ref string) string {
if ref == "" {
return baseURL
}
return baseURL + "?" + lfsRefQueryKey + "=" + url.QueryEscape(ref)
}
// UploadLink builds a URL to upload the object.
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
func (rc *requestContext) UploadLink(p lfs_module.Pointer, ref string) string {
base := rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
return appendRefQuery(base, ref)
}
// VerifyLink builds a URL for verifying the object.
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
return rc.RepoGitURL + "/info/lfs/verify"
func (rc *requestContext) VerifyLink(p lfs_module.Pointer, ref string) string {
return appendRefQuery(rc.RepoGitURL+"/info/lfs/verify", ref)
}
// CheckAcceptMediaType checks if the client accepts the LFS media type.
@ -113,7 +127,7 @@ func DownloadHandler(ctx *context.Context) {
rc := getRequestContext(ctx)
p := lfs_module.Pointer{Oid: ctx.PathParam("oid")}
meta := getAuthenticatedMeta(ctx, rc, p, false)
meta := getAuthenticatedMeta(ctx, rc, p, false, "")
if meta == nil {
return
}
@ -183,6 +197,13 @@ func DownloadHandler(ctx *context.Context) {
}
}
func refNameFromBatchRequest(br *lfs_module.BatchRequest) string {
if br.Ref == nil {
return ""
}
return br.Ref.Name
}
// BatchHandler provides the batch api
func BatchHandler(ctx *context.Context) {
var br lfs_module.BatchRequest
@ -205,8 +226,9 @@ func BatchHandler(ctx *context.Context) {
}
rc := getRequestContext(ctx)
refName := refNameFromBatchRequest(&br)
repository := getAuthenticatedRepository(ctx, rc, isUpload)
repository := getAuthenticatedRepository(ctx, rc, isUpload, refName)
if repository == nil {
return
}
@ -225,7 +247,7 @@ func BatchHandler(ctx *context.Context) {
responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
Code: http.StatusUnprocessableEntity,
Message: "Oid or size are invalid",
}))
}, refName))
continue
}
@ -247,7 +269,7 @@ func BatchHandler(ctx *context.Context) {
responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
Code: http.StatusUnprocessableEntity,
Message: fmt.Sprintf("Object %s is not %d bytes", p.Oid, p.Size),
}))
}, refName))
continue
}
@ -280,7 +302,7 @@ func BatchHandler(ctx *context.Context) {
}
}
responseObject = buildObjectResponse(rc, p, false, !exists, err)
responseObject = buildObjectResponse(rc, p, false, !exists, err, refName)
} else {
var err *lfs_module.ObjectError
if !exists || meta == nil {
@ -290,7 +312,7 @@ func BatchHandler(ctx *context.Context) {
}
}
responseObject = buildObjectResponse(rc, p, true, false, err)
responseObject = buildObjectResponse(rc, p, true, false, err, refName)
}
responseObjects = append(responseObjects, responseObject)
}
@ -307,6 +329,7 @@ func BatchHandler(ctx *context.Context) {
// UploadHandler receives data from the client and puts it into the content store
func UploadHandler(ctx *context.Context) {
ref := ctx.Req.URL.Query().Get(lfsRefQueryKey)
rc := getRequestContext(ctx)
p := lfs_module.Pointer{Oid: ctx.PathParam("oid")}
@ -321,7 +344,7 @@ func UploadHandler(ctx *context.Context) {
return
}
repository := getAuthenticatedRepository(ctx, rc, true)
repository := getAuthenticatedRepository(ctx, rc, true, ref)
if repository == nil {
return
}
@ -392,9 +415,10 @@ func VerifyHandler(ctx *context.Context) {
return
}
ref := ctx.Req.URL.Query().Get(lfsRefQueryKey)
rc := getRequestContext(ctx)
meta := getAuthenticatedMeta(ctx, rc, p, true)
meta := getAuthenticatedMeta(ctx, rc, p, true, ref)
if meta == nil {
return
}
@ -430,14 +454,14 @@ func getRequestContext(ctx *context.Context) *requestContext {
}
}
func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {
func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool, ref string) *git_model.LFSMetaObject {
if !p.IsValid() {
log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")
return nil
}
repository := getAuthenticatedRepository(ctx, rc, requireWrite)
repository := getAuthenticatedRepository(ctx, rc, requireWrite, ref)
if repository == nil {
return nil
}
@ -452,7 +476,7 @@ func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module
return meta
}
func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {
func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool, ref string) *repo_model.Repository {
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
@ -460,7 +484,7 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
return nil
}
if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
if !authenticate(ctx, repository, rc.Authorization, false, requireWrite, ref) {
requireAuth(ctx)
return nil
}
@ -478,7 +502,7 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
return repository
}
func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError) *lfs_module.ObjectResponse {
func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError, ref string) *lfs_module.ObjectResponse {
rep := &lfs_module.ObjectResponse{Pointer: pointer}
if err != nil {
rep.Error = err
@ -502,13 +526,13 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa
if upload {
// Set Transfer-Encoding header to enable chunked uploads. Required by git-lfs client to do chunked transfer.
// See: https://github.com/git-lfs/git-lfs/blob/main/tq/basic_upload.go#L58-59
rep.Actions["upload"] = lfs_module.NewLink(rc.UploadLink(pointer)).
rep.Actions["upload"] = lfs_module.NewLink(rc.UploadLink(pointer, ref)).
WithHeader("Authorization", rc.Authorization).
WithHeader("Transfer-Encoding", "chunked")
// "Accept" header is the workaround for git-lfs < 2.8.0 (before 2019).
// This workaround could be removed in the future: https://github.com/git-lfs/git-lfs/issues/3662
rep.Actions["verify"] = lfs_module.NewLink(rc.VerifyLink(pointer)).
rep.Actions["verify"] = lfs_module.NewLink(rc.VerifyLink(pointer, ref)).
WithHeader("Authorization", rc.Authorization).
WithHeader("Accept", lfs_module.AcceptHeader)
}
@ -532,9 +556,28 @@ func writeStatusMessage(ctx *context.Context, status int, message string) {
}
}
func canMaintainerWriteLFS(ctx *context.Context, perm access_model.Permission, user *user_model.User, ref string) bool {
if user == nil {
return false
}
refName := git.RefName(ref)
if refName == "" {
return false
}
branchName := refName.BranchName()
if branchName == "" {
if strings.HasPrefix(ref, "refs/") {
return false
}
branchName = ref
}
return issues_model.CanMaintainerWriteToBranch(ctx, perm, branchName, user)
}
// authenticate uses the authorization string to determine whether
// to proceed. This server assumes an HTTP Basic auth format.
func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool {
func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool, ref string) bool {
accessMode := perm_model.AccessModeRead
if requireWrite {
accessMode = perm_model.AccessModeWrite
@ -557,6 +600,9 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
}
canAccess := perm.CanAccess(accessMode, unit.TypeCode)
if requireWrite && !canAccess && canMaintainerWriteLFS(ctx, perm, ctx.Doer, ref) {
canAccess = true
}
// if it doesn't require sign-in and anonymous user has access, return true
// if the user is already signed in (for example: by session auth method), and the doer can access, return true
if canAccess && (!requireSigned || ctx.IsSigned) {
@ -573,7 +619,27 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
return false
}
ctx.Doer = user
return true
if !requireWrite {
return true
}
perm, err = access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
return false
}
canAccess = perm.CanAccess(accessMode, unit.TypeCode)
if !canAccess && canMaintainerWriteLFS(ctx, perm, ctx.Doer, ref) {
canAccess = true
}
if canAccess {
return true
}
log.Warn("Authentication failure for provided token: insufficient permissions for %s/%s", repository.OwnerName, repository.Name)
return false
}
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {

View File

@ -8,8 +8,11 @@ import (
"testing"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
@ -23,6 +26,7 @@ func TestMain(m *testing.M) {
func TestAuthenticate(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoFork := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1})
_, token2, _ = strings.Cut(token2, " ")
@ -44,8 +48,18 @@ func TestAuthenticate(t *testing.T) {
t.Run("authenticate", func(t *testing.T) {
const prefixBearer = "Bearer "
assert.False(t, authenticate(ctx, repo1, "", true, false))
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
assert.False(t, authenticate(ctx, repo1, "", true, false, ""))
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false, ""))
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false, ""))
})
t.Run("maintainer edits", func(t *testing.T) {
maintainer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
ctx2, _ := contexttest.MockContext(t, "/")
ctx2.Doer = maintainer
perm, err := access_model.GetUserRepoPermission(ctx2, repoFork, maintainer)
require.NoError(t, err)
require.False(t, perm.CanWrite(unit.TypeCode))
assert.True(t, authenticate(ctx2, repoFork, "", false, true, "branch2"))
})
}