Skip to content

Commit b1b635c

Browse files
Gustedearl-warren
authored andcommitted
fix(sec): permission check for project issue
- Do an access check when loading issues for a project column, currently this is not done and exposes the title, labels and existence of a private issue that the viewer of the project board may not have access to. - The number of issues cannot be calculated in a efficient manner and stored in the database because their number may vary depending on the visibility of the repositories participating in the project. The previous implementation used the pre-calculated numbers stored in each project, which did not reflect that potential variation. - The code is derived from go-gitea/gitea#22865
1 parent 9484502 commit b1b635c

File tree

7 files changed

+83
-43
lines changed

7 files changed

+83
-43
lines changed

models/issues/issue_project.go

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import (
77
"context"
88

99
"code.gitea.io/gitea/models/db"
10+
org_model "code.gitea.io/gitea/models/organization"
1011
project_model "code.gitea.io/gitea/models/project"
1112
user_model "code.gitea.io/gitea/models/user"
13+
"code.gitea.io/gitea/modules/optional"
1214
"code.gitea.io/gitea/modules/util"
1315
)
1416

@@ -48,22 +50,29 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
4850
}
4951

5052
// LoadIssuesFromColumn load issues assigned to this column
51-
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
52-
issueList, err := Issues(ctx, &IssuesOptions{
53+
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, doer *user_model.User, org *org_model.Organization, isClosed optional.Option[bool]) (IssueList, error) {
54+
issueOpts := &IssuesOptions{
5355
ProjectColumnID: b.ID,
5456
ProjectID: b.ProjectID,
5557
SortType: "project-column-sorting",
56-
})
58+
IsClosed: isClosed,
59+
}
60+
if doer != nil {
61+
issueOpts.User = doer
62+
issueOpts.Org = org
63+
} else {
64+
issueOpts.AllPublic = true
65+
}
66+
67+
issueList, err := Issues(ctx, issueOpts)
5768
if err != nil {
5869
return nil, err
5970
}
6071

6172
if b.Default {
62-
issues, err := Issues(ctx, &IssuesOptions{
63-
ProjectColumnID: db.NoConditionID,
64-
ProjectID: b.ProjectID,
65-
SortType: "project-column-sorting",
66-
})
73+
issueOpts.ProjectColumnID = db.NoConditionID
74+
75+
issues, err := Issues(ctx, issueOpts)
6776
if err != nil {
6877
return nil, err
6978
}
@@ -78,10 +87,10 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueLi
7887
}
7988

8089
// LoadIssuesFromColumnList load issues assigned to the columns
81-
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
90+
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, doer *user_model.User, org *org_model.Organization, isClosed optional.Option[bool]) (map[int64]IssueList, error) {
8291
issuesMap := make(map[int64]IssueList, len(bs))
8392
for i := range bs {
84-
il, err := LoadIssuesFromColumn(ctx, bs[i])
93+
il, err := LoadIssuesFromColumn(ctx, bs[i], doer, org, isClosed)
8594
if err != nil {
8695
return nil, err
8796
}
@@ -160,3 +169,36 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
160169
})
161170
})
162171
}
172+
173+
// NumIssuesInProjects returns the amount of issues assigned to one of the project
174+
// in the list which the doer can access.
175+
func NumIssuesInProjects(ctx context.Context, pl []*project_model.Project, doer *user_model.User, org *org_model.Organization, isClosed optional.Option[bool]) (map[int64]int, error) {
176+
numMap := make(map[int64]int, len(pl))
177+
for _, p := range pl {
178+
num, err := NumIssuesInProject(ctx, p, doer, org, isClosed)
179+
if err != nil {
180+
return nil, err
181+
}
182+
numMap[p.ID] = num
183+
}
184+
185+
return numMap, nil
186+
}
187+
188+
// NumIssuesInProject returns the amount of issues assigned to the project which
189+
// the doer can access.
190+
func NumIssuesInProject(ctx context.Context, p *project_model.Project, doer *user_model.User, org *org_model.Organization, isClosed optional.Option[bool]) (int, error) {
191+
numIssuesInProject := int(0)
192+
bs, err := p.GetColumns(ctx)
193+
if err != nil {
194+
return 0, err
195+
}
196+
im, err := LoadIssuesFromColumnList(ctx, bs, doer, org, isClosed)
197+
if err != nil {
198+
return 0, err
199+
}
200+
for _, il := range im {
201+
numIssuesInProject += len(il)
202+
}
203+
return numIssuesInProject, nil
204+
}

models/project/column.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,6 @@ func (Column) TableName() string {
5757
return "project_board" // TODO: the legacy table name should be project_column
5858
}
5959

60-
// NumIssues return counter of all issues assigned to the column
61-
func (c *Column) NumIssues(ctx context.Context) int {
62-
total, err := db.GetEngine(ctx).Table("project_issue").
63-
Where("project_id=?", c.ProjectID).
64-
And("project_board_id=?", c.ID).
65-
GroupBy("issue_id").
66-
Cols("issue_id").
67-
Count()
68-
if err != nil {
69-
return 0
70-
}
71-
return int(total)
72-
}
73-
7460
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
7561
issues := make([]*ProjectIssue, 0, 5)
7662
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).

models/project/issue.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
3434
return err
3535
}
3636

37-
// NumIssues return counter of all issues assigned to a project
38-
func (p *Project) NumIssues(ctx context.Context) int {
39-
c, err := db.GetEngine(ctx).Table("project_issue").
40-
Where("project_id=?", p.ID).
41-
GroupBy("issue_id").
42-
Cols("issue_id").
43-
Count()
44-
if err != nil {
45-
log.Error("NumIssues: %v", err)
46-
return 0
47-
}
48-
return int(c)
49-
}
50-
5137
// NumClosedIssues return counter of closed issues assigned to a project
5238
func (p *Project) NumClosedIssues(ctx context.Context) int {
5339
c, err := db.GetEngine(ctx).Table("project_issue").

routers/web/org/projects.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ func Projects(ctx *context.Context) {
126126
ctx.Data["PageIsViewProjects"] = true
127127
ctx.Data["SortType"] = sortType
128128

129+
numOpenIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(false))
130+
if err != nil {
131+
ctx.ServerError("NumIssuesInProjects", err)
132+
return
133+
}
134+
numClosedIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(true))
135+
if err != nil {
136+
ctx.ServerError("NumIssuesInProjects", err)
137+
return
138+
}
139+
ctx.Data["NumOpenIssuesInProject"] = numOpenIssues
140+
ctx.Data["NumClosedIssuesInProject"] = numClosedIssues
141+
129142
ctx.HTML(http.StatusOK, tplProjects)
130143
}
131144

@@ -332,7 +345,7 @@ func ViewProject(ctx *context.Context) {
332345
return
333346
}
334347

335-
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
348+
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, ctx.Doer, ctx.Org.Organization, optional.None[bool]())
336349
if err != nil {
337350
ctx.ServerError("LoadIssuesOfColumns", err)
338351
return

routers/web/repo/projects.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ func Projects(ctx *context.Context) {
125125
ctx.Data["IsProjectsPage"] = true
126126
ctx.Data["SortType"] = sortType
127127

128+
numOpenIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(false))
129+
if err != nil {
130+
ctx.ServerError("NumIssuesInProjects", err)
131+
return
132+
}
133+
numClosedIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(true))
134+
if err != nil {
135+
ctx.ServerError("NumIssuesInProjects", err)
136+
return
137+
}
138+
ctx.Data["NumOpenIssuesInProject"] = numOpenIssues
139+
ctx.Data["NumClosedIssuesInProject"] = numClosedIssues
140+
128141
ctx.HTML(http.StatusOK, tplProjects)
129142
}
130143

@@ -310,7 +323,7 @@ func ViewProject(ctx *context.Context) {
310323
return
311324
}
312325

313-
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
326+
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, ctx.Doer, nil, optional.None[bool]())
314327
if err != nil {
315328
ctx.ServerError("LoadIssuesOfColumns", err)
316329
return

templates/projects/list.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@
4949
<div class="group">
5050
<div class="flex-text-block">
5151
{{svg "octicon-issue-opened" 14}}
52-
{{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
52+
{{ctx.Locale.PrettyNumber (index $.NumOpenIssuesInProject .ID)}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
5353
</div>
5454
<div class="flex-text-block">
5555
{{svg "octicon-check" 14}}
56-
{{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
56+
{{ctx.Locale.PrettyNumber (index $.NumClosedIssuesInProject .ID)}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
5757
</div>
5858
</div>
5959
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}

templates/projects/view.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
7171
<div class="ui large label project-column-title tw-py-1">
7272
<div class="ui small circular grey label project-column-issue-count">
73-
{{.NumIssues ctx}}
73+
{{len (index $.IssuesMap .ID)}}
7474
</div>
7575
<span class="project-column-title-label">{{.Title}}</span>
7676
</div>

0 commit comments

Comments
 (0)