Skip to content

Commit 19f3639

Browse files
author
0ko
committed
fix(sec): permission check for project issue (go-gitea#6843) (merge commit)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6843 Reviewed-by: 0ko <[email protected]>
2 parents 9484502 + 51060d9 commit 19f3639

File tree

13 files changed

+325
-43
lines changed

13 files changed

+325
-43
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-
2+
id: 1001
3+
title: Org project that contains private issues
4+
owner_id: 3
5+
repo_id: 0
6+
is_closed: false
7+
creator_id: 2
8+
board_type: 1
9+
type: 3
10+
created_unix: 1738000000
11+
updated_unix: 1738000000
12+
13+
-
14+
id: 1002
15+
title: User project that contains private issues
16+
owner_id: 2
17+
repo_id: 0
18+
is_closed: false
19+
creator_id: 2
20+
board_type: 1
21+
type: 1
22+
created_unix: 1738000000
23+
updated_unix: 1738000000
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-
2+
id: 1001
3+
project_id: 1001
4+
title: Triage
5+
creator_id: 2
6+
default: true
7+
created_unix: 1738000000
8+
updated_unix: 1738000000
9+
10+
-
11+
id: 1002
12+
project_id: 1002
13+
title: Triage
14+
creator_id: 2
15+
default: true
16+
created_unix: 1738000000
17+
updated_unix: 1738000000
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-
2+
id: 1001
3+
issue_id: 6
4+
project_id: 1001
5+
project_board_id: 1001
6+
7+
-
8+
id: 1002
9+
issue_id: 7
10+
project_id: 1002
11+
project_board_id: 1002

models/fixtures/team_unit.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,49 @@
11
-
22
id: 1
33
team_id: 1
4+
org_id: 3
45
type: 1
56
access_mode: 4
67

78
-
89
id: 2
910
team_id: 1
11+
org_id: 3
1012
type: 2
1113
access_mode: 4
1214

1315
-
1416
id: 3
1517
team_id: 1
18+
org_id: 3
1619
type: 3
1720
access_mode: 4
1821

1922
-
2023
id: 4
2124
team_id: 1
25+
org_id: 3
2226
type: 4
2327
access_mode: 4
2428

2529
-
2630
id: 5
2731
team_id: 1
32+
org_id: 3
2833
type: 5
2934
access_mode: 4
3035

3136
-
3237
id: 6
3338
team_id: 1
39+
org_id: 3
3440
type: 6
3541
access_mode: 4
3642

3743
-
3844
id: 7
3945
team_id: 1
46+
org_id: 3
4047
type: 7
4148
access_mode: 4
4249

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+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2025 The Forgejo Authors. All rights reserved.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
package issues_test
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
"code.gitea.io/gitea/models/issues"
11+
"code.gitea.io/gitea/models/organization"
12+
"code.gitea.io/gitea/models/project"
13+
"code.gitea.io/gitea/models/unittest"
14+
user_model "code.gitea.io/gitea/models/user"
15+
"code.gitea.io/gitea/modules/optional"
16+
"code.gitea.io/gitea/tests"
17+
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
)
21+
22+
func TestPrivateIssueProjects(t *testing.T) {
23+
defer tests.AddFixtures("models/fixtures/PrivateIssueProjects/")()
24+
require.NoError(t, unittest.PrepareTestDatabase())
25+
26+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
27+
t.Run("Organization project", func(t *testing.T) {
28+
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
29+
orgProject := unittest.AssertExistsAndLoadBean(t, &project.Project{ID: 1001, OwnerID: org.ID})
30+
column := unittest.AssertExistsAndLoadBean(t, &project.Column{ID: 1001, ProjectID: orgProject.ID})
31+
32+
t.Run("Authenticated user", func(t *testing.T) {
33+
defer tests.PrintCurrentTest(t)()
34+
issueList, err := issues.LoadIssuesFromColumn(db.DefaultContext, column, user2, org, optional.None[bool]())
35+
require.NoError(t, err)
36+
assert.Len(t, issueList, 1)
37+
assert.EqualValues(t, 6, issueList[0].ID)
38+
39+
issuesNum, err := issues.NumIssuesInProject(db.DefaultContext, orgProject, user2, org, optional.None[bool]())
40+
require.NoError(t, err)
41+
assert.EqualValues(t, 1, issuesNum)
42+
43+
issuesNum, err = issues.NumIssuesInProject(db.DefaultContext, orgProject, user2, org, optional.Some(true))
44+
require.NoError(t, err)
45+
assert.EqualValues(t, 0, issuesNum)
46+
47+
issuesNum, err = issues.NumIssuesInProject(db.DefaultContext, orgProject, user2, org, optional.Some(false))
48+
require.NoError(t, err)
49+
assert.EqualValues(t, 1, issuesNum)
50+
})
51+
52+
t.Run("Anonymous user", func(t *testing.T) {
53+
defer tests.PrintCurrentTest(t)()
54+
issueList, err := issues.LoadIssuesFromColumn(db.DefaultContext, column, nil, org, optional.None[bool]())
55+
require.NoError(t, err)
56+
assert.Empty(t, issueList)
57+
58+
issuesNum, err := issues.NumIssuesInProject(db.DefaultContext, orgProject, nil, org, optional.None[bool]())
59+
require.NoError(t, err)
60+
assert.EqualValues(t, 0, issuesNum)
61+
})
62+
})
63+
64+
t.Run("User project", func(t *testing.T) {
65+
userProject := unittest.AssertExistsAndLoadBean(t, &project.Project{ID: 1002, OwnerID: user2.ID})
66+
column := unittest.AssertExistsAndLoadBean(t, &project.Column{ID: 1002, ProjectID: userProject.ID})
67+
68+
t.Run("Authenticated user", func(t *testing.T) {
69+
defer tests.PrintCurrentTest(t)()
70+
issueList, err := issues.LoadIssuesFromColumn(db.DefaultContext, column, user2, nil, optional.None[bool]())
71+
require.NoError(t, err)
72+
assert.Len(t, issueList, 1)
73+
assert.EqualValues(t, 7, issueList[0].ID)
74+
75+
issuesNum, err := issues.NumIssuesInProject(db.DefaultContext, userProject, user2, nil, optional.None[bool]())
76+
require.NoError(t, err)
77+
assert.EqualValues(t, 1, issuesNum)
78+
79+
issuesNum, err = issues.NumIssuesInProject(db.DefaultContext, userProject, user2, nil, optional.Some(true))
80+
require.NoError(t, err)
81+
assert.EqualValues(t, 0, issuesNum)
82+
83+
issuesNum, err = issues.NumIssuesInProject(db.DefaultContext, userProject, user2, nil, optional.Some(false))
84+
require.NoError(t, err)
85+
assert.EqualValues(t, 1, issuesNum)
86+
})
87+
88+
t.Run("Anonymous user", func(t *testing.T) {
89+
defer tests.PrintCurrentTest(t)()
90+
91+
issueList, err := issues.LoadIssuesFromColumn(db.DefaultContext, column, nil, nil, optional.None[bool]())
92+
require.NoError(t, err)
93+
assert.Empty(t, issueList)
94+
95+
issuesNum, err := issues.NumIssuesInProject(db.DefaultContext, userProject, nil, nil, optional.None[bool]())
96+
require.NoError(t, err)
97+
assert.EqualValues(t, 0, issuesNum)
98+
})
99+
})
100+
}

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

0 commit comments

Comments
 (0)