detached ddab9a2

fix(rpc): answer not found to non-owners before parsing moderation requests

commit
ddab9a2132ce597552bf742634282cb85f6fc741
author
brice · · 2026-10-19 08:57 UTC
parent
f8b1e92
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +30 -0
M internal/adapters/rpc/issue.go +19 -0
@@ -198,7 +198,20 @@ func pendingRef(repo, id string) (domain.RepoName, domain.PendingIssueID, error)
198198 return name, pid, nil
199199 }
200200
201+// requireOwner answers NotFound to anyone without owner rights, before the
202+// request is parsed: a malformed repository must not tell a non-owner more
203+// than a well-formed one does.
204+func requireOwner(ctx context.Context) error {
205+ if !caller(ctx).HasOwnerRights() {
206+ return connectError(ctx, domain.ErrNotFound)
207+ }
208+ return nil
209+}
210+
201211 func (s *issueService) ListPendingIssues(ctx context.Context, req *connect.Request[pb.ListPendingIssuesRequest]) (*connect.Response[pb.ListPendingIssuesResponse], error) {
212+ if err := requireOwner(ctx); err != nil {
213+ return nil, err
214+ }
202215 var name domain.RepoName
203216 if req.Msg.GetRepo() != "" {
204217 var err error
@@ -220,6 +233,9 @@ func (s *issueService) ListPendingIssues(ctx context.Context, req *connect.Reque
220233 }
221234
222235 func (s *issueService) ApproveIssue(ctx context.Context, req *connect.Request[pb.ApproveIssueRequest]) (*connect.Response[pb.ApproveIssueResponse], error) {
236+ if err := requireOwner(ctx); err != nil {
237+ return nil, err
238+ }
223239 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())
224240 if err != nil {
225241 return nil, connectError(ctx, err)
@@ -232,6 +248,9 @@ func (s *issueService) ApproveIssue(ctx context.Context, req *connect.Request[pb
232248 }
233249
234250 func (s *issueService) RejectIssue(ctx context.Context, req *connect.Request[pb.RejectIssueRequest]) (*connect.Response[pb.RejectIssueResponse], error) {
251+ if err := requireOwner(ctx); err != nil {
252+ return nil, err
253+ }
235254 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())
236255 if err != nil {
237256 return nil, connectError(ctx, err)
@@ -198,7 +198,20 @@ func pendingRef(repo, id string) (domain.RepoName, domain.PendingIssueID, error)
198 return name, pid, nil198 return name, pid, nil
199 }199 }
200 200
201+// requireOwner answers NotFound to anyone without owner rights, before the
202+// request is parsed: a malformed repository must not tell a non-owner more
203+// than a well-formed one does.
204+func requireOwner(ctx context.Context) error {
205+ if !caller(ctx).HasOwnerRights() {
206+ return connectError(ctx, domain.ErrNotFound)
207+ }
208+ return nil
209+}
210+
201 func (s *issueService) ListPendingIssues(ctx context.Context, req *connect.Request[pb.ListPendingIssuesRequest]) (*connect.Response[pb.ListPendingIssuesResponse], error) {211 func (s *issueService) ListPendingIssues(ctx context.Context, req *connect.Request[pb.ListPendingIssuesRequest]) (*connect.Response[pb.ListPendingIssuesResponse], error) {
212+ if err := requireOwner(ctx); err != nil {
213+ return nil, err
214+ }
202 var name domain.RepoName215 var name domain.RepoName
203 if req.Msg.GetRepo() != "" {216 if req.Msg.GetRepo() != "" {
204 var err error217 var err error
@@ -220,6 +233,9 @@ func (s *issueService) ListPendingIssues(ctx context.Context, req *connect.Reque
220 }233 }
221 234
222 func (s *issueService) ApproveIssue(ctx context.Context, req *connect.Request[pb.ApproveIssueRequest]) (*connect.Response[pb.ApproveIssueResponse], error) {235 func (s *issueService) ApproveIssue(ctx context.Context, req *connect.Request[pb.ApproveIssueRequest]) (*connect.Response[pb.ApproveIssueResponse], error) {
236+ if err := requireOwner(ctx); err != nil {
237+ return nil, err
238+ }
223 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())239 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())
224 if err != nil {240 if err != nil {
225 return nil, connectError(ctx, err)241 return nil, connectError(ctx, err)
@@ -232,6 +248,9 @@ func (s *issueService) ApproveIssue(ctx context.Context, req *connect.Request[pb
232 }248 }
233 249
234 func (s *issueService) RejectIssue(ctx context.Context, req *connect.Request[pb.RejectIssueRequest]) (*connect.Response[pb.RejectIssueResponse], error) {250 func (s *issueService) RejectIssue(ctx context.Context, req *connect.Request[pb.RejectIssueRequest]) (*connect.Response[pb.RejectIssueResponse], error) {
251+ if err := requireOwner(ctx); err != nil {
252+ return nil, err
253+ }
235 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())254 name, id, err := pendingRef(req.Msg.GetRepo(), req.Msg.GetId())
236 if err != nil {255 if err != nil {
237 return nil, connectError(ctx, err)256 return nil, connectError(ctx, err)
M internal/app/issue_moderation_api_test.go +11 -0
@@ -52,6 +52,13 @@ func TestIssueModerationOverTheAPI(t *testing.T) {
5252 assert.Equal(t, "not_found", codeName(err), "%s approve", name)
5353 _, err = c.issue.RejectIssue(ctx, connect.NewRequest(&pb.RejectIssueRequest{Repo: "pub", Id: p1.ID.String(), Ban: true}))
5454 assert.Equal(t, "not_found", codeName(err), "%s reject", name)
55+ // A malformed repository tells a non-owner nothing more.
56+ _, err = c.issue.ListPendingIssues(ctx, connect.NewRequest(&pb.ListPendingIssuesRequest{Repo: "Bad Repo!"}))
57+ assert.Equal(t, "not_found", codeName(err), "%s list, malformed repo", name)
58+ _, err = c.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "Bad Repo!", Id: p1.ID.String()}))
59+ assert.Equal(t, "not_found", codeName(err), "%s approve, malformed repo", name)
60+ _, err = c.issue.RejectIssue(ctx, connect.NewRequest(&pb.RejectIssueRequest{Repo: "Bad Repo!", Id: "nope"}))
61+ assert.Equal(t, "not_found", codeName(err), "%s reject, malformed repo", name)
5562 }
5663 still, err := r.forge.PendingIssues.Count(ctx)
5764 require.NoError(t, err)
@@ -68,6 +75,10 @@ func TestIssueModerationOverTheAPI(t *testing.T) {
6875 require.NoError(t, err)
6976 assert.Empty(t, res.Msg.GetIssues())
7077
78+ // An owner does learn that a repository name is malformed.
79+ _, err = owner.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "Bad Repo!", Id: p1.ID.String()}))
80+ assert.Equal(t, "invalid_argument", codeName(err))
81+
7182 // A malformed or unknown id is NotFound.
7283 _, err = owner.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "pub", Id: "nope"}))
7384 assert.Equal(t, "not_found", codeName(err))
@@ -52,6 +52,13 @@ func TestIssueModerationOverTheAPI(t *testing.T) {
52 assert.Equal(t, "not_found", codeName(err), "%s approve", name)52 assert.Equal(t, "not_found", codeName(err), "%s approve", name)
53 _, err = c.issue.RejectIssue(ctx, connect.NewRequest(&pb.RejectIssueRequest{Repo: "pub", Id: p1.ID.String(), Ban: true}))53 _, err = c.issue.RejectIssue(ctx, connect.NewRequest(&pb.RejectIssueRequest{Repo: "pub", Id: p1.ID.String(), Ban: true}))
54 assert.Equal(t, "not_found", codeName(err), "%s reject", name)54 assert.Equal(t, "not_found", codeName(err), "%s reject", name)
55+ // A malformed repository tells a non-owner nothing more.
56+ _, err = c.issue.ListPendingIssues(ctx, connect.NewRequest(&pb.ListPendingIssuesRequest{Repo: "Bad Repo!"}))
57+ assert.Equal(t, "not_found", codeName(err), "%s list, malformed repo", name)
58+ _, err = c.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "Bad Repo!", Id: p1.ID.String()}))
59+ assert.Equal(t, "not_found", codeName(err), "%s approve, malformed repo", name)
60+ _, err = c.issue.RejectIssue(ctx, connect.NewRequest(&pb.RejectIssueRequest{Repo: "Bad Repo!", Id: "nope"}))
61+ assert.Equal(t, "not_found", codeName(err), "%s reject, malformed repo", name)
55 }62 }
56 still, err := r.forge.PendingIssues.Count(ctx)63 still, err := r.forge.PendingIssues.Count(ctx)
57 require.NoError(t, err)64 require.NoError(t, err)
@@ -68,6 +75,10 @@ func TestIssueModerationOverTheAPI(t *testing.T) {
68 require.NoError(t, err)75 require.NoError(t, err)
69 assert.Empty(t, res.Msg.GetIssues())76 assert.Empty(t, res.Msg.GetIssues())
70 77
78+ // An owner does learn that a repository name is malformed.
79+ _, err = owner.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "Bad Repo!", Id: p1.ID.String()}))
80+ assert.Equal(t, "invalid_argument", codeName(err))
81+
71 // A malformed or unknown id is NotFound.82 // A malformed or unknown id is NotFound.
72 _, err = owner.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "pub", Id: "nope"}))83 _, err = owner.issue.ApproveIssue(ctx, connect.NewRequest(&pb.ApproveIssueRequest{Repo: "pub", Id: "nope"}))
73 assert.Equal(t, "not_found", codeName(err))84 assert.Equal(t, "not_found", codeName(err))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close