fix(projects): hide linked pull requests the viewer cannot read (#40016) (#40134)

Backport #40016 by @basavaraj-sm05

Project board cards list the pull requests that reference their issue,
but loaded them without checking the viewer's access. A private pull
request mentioning a public issue showed its title, number and state on
that issue's board to anyone who can view the board, anonymous visitors
included.

Repository and organization boards now skip linked pull requests in
repositories where the viewer cannot read pull requests, matching how
the issue page already hides such references.

Co-authored-by: Basavaraj S m <basavaraj@digiscrypt.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
authored and GitHub committed 2026-10-31 18:14:26 +01:00
1 parent 5a3f06430d
commit a1c56dd96a
4 files changed
+68 -38

No files matched your search

+4 -19
View File
@@ -365,25 +365,10 @@ func ViewProject(ctx *context.Context) {
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
}
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIDs,
IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
}
}
linkedPrsMap, err := project_service.LoadLinkedPullRequests(ctx, issuesMap, ctx.Doer)
if err != nil {
ctx.ServerError("LoadLinkedPullRequests", err)
return
}
// TODO: Add option to filter also by repository specific labels
+4 -19
View File
@@ -343,25 +343,10 @@ func ViewProject(ctx *context.Context) {
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
}
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIDs,
IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
}
}
linkedPrsMap, err := project_service.LoadLinkedPullRequests(ctx, issuesMap, ctx.Doer)
if err != nil {
ctx.ServerError("LoadLinkedPullRequests", err)
return
}
ctx.Data["LinkedPRs"] = linkedPrsMap
+43
View File
@@ -10,6 +10,7 @@ import (
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
project_model "gitea.dev/models/project"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
@@ -208,6 +209,48 @@ func LoadIssuesFromProject(ctx context.Context, project *project_model.Project,
return results, nil
}
// LoadLinkedPullRequests returns, per issue, the pull requests referencing it that the doer can read
func LoadLinkedPullRequests(ctx context.Context, issuesMap map[int64]issues_model.IssueList, doer *user_model.User) (map[int64][]*issues_model.Issue, error) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
canReadPulls := make(map[int64]bool)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
if len(referencedIDs) == 0 {
continue
}
linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIDs,
IsPull: optional.Some(true),
})
if err != nil {
return nil, err
}
for _, pr := range linkedPrs {
canRead, ok := canReadPulls[pr.RepoID]
if !ok {
perm, err := access_model.GetDoerRepoPermission(ctx, pr.Repo, doer)
if err != nil {
return nil, err
}
canRead = perm.CanReadIssuesOrPulls(true)
canReadPulls[pr.RepoID] = canRead
}
if canRead {
linkedPrsMap[issue.ID] = append(linkedPrsMap[issue.ID], pr)
}
}
}
}
return linkedPrsMap, nil
}
// NumClosedIssues return counter of closed issues assigned to a project
func loadNumClosedIssues(ctx context.Context, p *project_model.Project) error {
cnt, err := db.GetEngine(ctx).Table("project_issue").
+17
View File
@@ -379,6 +379,23 @@ func TestOrgProjectFilterByMilestone(t *testing.T) {
func TestProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("PrivateLinkedPullRequest", func(t *testing.T) {
require.NoError(t, db.Insert(t.Context(), &issues_model.Comment{
Type: issues_model.CommentTypePullRef,
PosterID: 2,
IssueID: 1,
RefRepoID: 3,
RefIssueID: 12,
RefIsPull: true,
}))
const linkedPR = `href="/org3/repo3/pulls/2"`
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/projects/1"), http.StatusOK)
assert.NotContains(t, resp.Body.String(), linkedPR)
resp = loginUser(t, "user2").MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/projects/1"), http.StatusOK)
assert.Contains(t, resp.Body.String(), linkedPR)
})
t.Run("LoadIssuesAssigneesForProject", func(t *testing.T) {
_ = db.TruncateBeans(t.Context(), "project_issue", "issue_assignees")
_ = db.Insert(t.Context(),