diff --git a/modules/lfs/shared.go b/modules/lfs/shared.go index cd9488e3db..e04c089e51 100644 --- a/modules/lfs/shared.go +++ b/modules/lfs/shared.go @@ -66,6 +66,21 @@ type Link struct { ExpiresAt *time.Time `json:"expires_at,omitempty"` } +func NewLink(href string) *Link { + return &Link{Href: href} +} + +func (l *Link) WithHeader(k, v string) *Link { + if v == "" { + return l + } + if l.Header == nil { + l.Header = make(map[string]string) + } + l.Header[k] = v + return l +} + // ObjectError defines the JSON structure returned to the client in case of an error. type ObjectError struct { Code int `json:"code"` diff --git a/services/convert/notification.go b/services/convert/notification.go index 69470638be..87166501a6 100644 --- a/services/convert/notification.go +++ b/services/convert/notification.go @@ -8,8 +8,8 @@ import ( "net/url" activities_model "code.gitea.io/gitea/models/activities" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" ) @@ -25,11 +25,17 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification) // since user only get notifications when he has access to use minimal access mode if n.Repository != nil { - result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead}) - - // This permission is not correct and we should not be reporting it - for repository := result.Repository; repository != nil; repository = repository.Parent { - repository.Permissions = nil + perm, err := access_model.GetUserRepoPermission(ctx, n.Repository, n.User) + if err != nil { + log.Error("GetUserRepoPermission failed: %v", err) + return result + } + if perm.HasAnyUnitAccessOrPublicAccess() { // if user has been revoked access to repo, do not show repo info + result.Repository = ToRepo(ctx, n.Repository, perm) + // This permission is not correct and we should not be reporting it + for repository := result.Repository; repository != nil; repository = repository.Parent { + repository.Permissions = nil + } } } diff --git a/services/convert/notification_test.go b/services/convert/notification_test.go new file mode 100644 index 0000000000..718a070819 --- /dev/null +++ b/services/convert/notification_test.go @@ -0,0 +1,57 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + activities_model "code.gitea.io/gitea/models/activities" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func TestToNotificationThreadIncludesRepoForAccessibleUser(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + n := newRepoNotification(t, 1, 4) + thread := ToNotificationThread(t.Context(), n) + + if assert.NotNil(t, thread.Repository) { + assert.Equal(t, n.Repository.FullName(), thread.Repository.FullName) + assert.Nil(t, thread.Repository.Permissions) + } +} + +func TestToNotificationThreadOmitsRepoWhenAccessRevoked(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + n := newRepoNotification(t, 2, 4) + thread := ToNotificationThread(t.Context(), n) + + assert.Nil(t, thread.Repository) +} + +func newRepoNotification(t *testing.T, repoID, userID int64) *activities_model.Notification { + t.Helper() + + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) + assert.NoError(t, repo.LoadOwner(ctx)) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + + return &activities_model.Notification{ + ID: repoID*1000 + userID, + UserID: user.ID, + RepoID: repo.ID, + Status: activities_model.NotificationStatusUnread, + Source: activities_model.NotificationSourceRepository, + UpdatedUnix: timeutil.TimeStampNow(), + Repository: repo, + User: user, + } +} diff --git a/services/lfs/server.go b/services/lfs/server.go index 3455b4b9bd..4819437bf1 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - "maps" "net/http" "net/url" "regexp" @@ -487,40 +486,32 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa rep.Error = err } else { rep.Actions = make(map[string]*lfs_module.Link) - - header := make(map[string]string) - - if len(rc.Authorization) > 0 { - header["Authorization"] = rc.Authorization - } - if download { var link *lfs_module.Link if setting.LFS.Storage.ServeDirect() { // If we have a signed url (S3, object storage), redirect to this directly. u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, rc.Method, nil) if u != nil && err == nil { - // Presigned url does not need the Authorization header - // https://github.com/go-gitea/gitea/issues/21525 - delete(header, "Authorization") - link = &lfs_module.Link{Href: u.String(), Header: header} + link = lfs_module.NewLink(u.String()) // Presigned url does not need the Authorization header } } if link == nil { - link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header} + link = lfs_module.NewLink(rc.DownloadLink(pointer)).WithHeader("Authorization", rc.Authorization) } rep.Actions["download"] = link } if upload { - rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header} + // 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)). + WithHeader("Authorization", rc.Authorization). + WithHeader("Transfer-Encoding", "chunked") - verifyHeader := make(map[string]string) - maps.Copy(verifyHeader, header) - - // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 - verifyHeader["Accept"] = lfs_module.AcceptHeader - - rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader} + // "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)). + WithHeader("Authorization", rc.Authorization). + WithHeader("Accept", lfs_module.AcceptHeader) } } return rep diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index fb55d311cc..86d5f69b9c 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -317,6 +317,7 @@ func TestAPILFSBatch(t *testing.T) { ul := br.Objects[0].Actions["upload"] assert.NotNil(t, ul) assert.NotEmpty(t, ul.Href) + assert.Equal(t, "chunked", ul.Header["Transfer-Encoding"], "git-lfs client needs Transfer-Encoding to do chunked transfer") assert.Contains(t, br.Objects[0].Actions, "verify") vl := br.Objects[0].Actions["verify"] assert.NotNil(t, vl)