From 269b116b1b04cb01fff34315ed450390244a0ebe Mon Sep 17 00:00:00 2001 From: Basavaraj S m Date: Fri, 23 Oct 2026 01:55:44 +0530 Subject: [PATCH] fix(feed): hide release notes in tags feed without releases access (#39944) The tags feed (`tags.rss` / `tags.atom`) lives in the tags route group, which only requires code read access, yet it lists every published release next to the plain tags and includes each release's title and rendered note. The releases page and `releases.rss` require releases read access, and the tags page only shows tag names to users without it, so the feed is the one place that leaks. A team member granted Code but not Releases on a private repo, or anyone on a public repo whose Releases unit is disabled, can read all release notes this way. I noticed it while comparing the feed routes with the tags template. When the doer cannot read releases, the feed now lists releases as plain tags, linking to `/src/tag/` like the tags page does. --- routers/web/feed/convert.go | 29 ++++++++++++++++------------- tests/integration/feed_repo_test.go | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 244298790b5..ccdd250a3b2 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,6 +15,7 @@ import ( activities_model "gitea.dev/models/activities" "gitea.dev/models/renderhelper" repo_model "gitea.dev/models/repo" + "gitea.dev/models/unit" "gitea.dev/modules/markup" "gitea.dev/modules/markup/markdown" "gitea.dev/modules/setting" @@ -276,27 +277,29 @@ func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string // feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (items []*feeds.Item, err error) { + canReadReleases := ctx.Repo.Permission.CanRead(unit.TypeReleases) for _, rel := range releases { err := rel.LoadAttributes(ctx) if err != nil { return nil, err } - var title string var content template.HTML - - if rel.IsTag { - title = rel.TagName - } else { - title = rel.Title - } - + title := rel.TagName link := &feeds.Link{Href: rel.HTMLURL()} - rctx := renderhelper.NewRenderContextRepoComment(ctx, rel.Repo).WithUseAbsoluteLink(true) - content, err = markdown.RenderString(rctx, - rel.Note) - if err != nil { - return nil, err + + if !canReadReleases { + // the tags feed only requires code access, so list releases as plain tags like the tags page does + link.Href = rel.Repo.HTMLURL() + "/src/tag/" + util.PathEscapeSegments(rel.TagName) + } else { + if !rel.IsTag { + title = rel.Title + } + rctx := renderhelper.NewRenderContextRepoComment(ctx, rel.Repo).WithUseAbsoluteLink(true) + content, err = markdown.RenderString(rctx, rel.Note) + if err != nil { + return nil, err + } } items = append(items, &feeds.Item{ diff --git a/tests/integration/feed_repo_test.go b/tests/integration/feed_repo_test.go index ffe908c74bd..dcce2cfdb11 100644 --- a/tests/integration/feed_repo_test.go +++ b/tests/integration/feed_repo_test.go @@ -9,9 +9,14 @@ import ( "testing" auth_model "gitea.dev/models/auth" + repo_model "gitea.dev/models/repo" + unit_model "gitea.dev/models/unit" + "gitea.dev/models/unittest" + repo_service "gitea.dev/services/repository" "gitea.dev/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFeedRepo(t *testing.T) { @@ -33,6 +38,19 @@ func TestFeedRepo(t *testing.T) { assert.Equal(t, "issue5", rss.Channel.Items[0].Description) assert.NotEmpty(t, rss.Channel.Items[0].PubDate) }) + + t.Run("TagsWithoutReleasesAccess", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + require.NoError(t, repo_service.UpdateRepositoryUnits(t.Context(), repo, nil, []unit_model.Type{unit_model.TypeReleases})) + + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/tags.rss"), http.StatusOK) + data := resp.Body.String() + assert.Contains(t, data, "/user2/repo1/src/tag/v1.0") + assert.NotContains(t, data, "pre-release") + assert.NotContains(t, data, "some text for a pre release") + }) } // TestFeedRepoContentTokenScopes ensures repository feed endpoints enforce the