Skip to content

Commit 1a3eaff

Browse files
FIX: Ensure review actions are only performable on visible topics (#276)
It's rare for code-review topics to be private, and it's even rarer for the review status to be considered sensitive. But even so, it makes sense to ensure reviewers can only change review status of topics they can see.
1 parent c379d34 commit 1a3eaff

6 files changed

Lines changed: 241 additions & 58 deletions

File tree

app/controllers/discourse_code_review/code_review_controller.rb

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def webhook
9393
end
9494

9595
def skip
96-
topic = Topic.find_by(id: params[:topic_id])
96+
topic = find_reviewable_commit_topic
9797

9898
State::CommitApproval.skip(topic, current_user)
9999

@@ -103,7 +103,7 @@ def skip
103103
def followup
104104
raise Discourse::InvalidAccess if !SiteSetting.code_review_allow_manual_followup
105105

106-
topic = Topic.find_by(id: params[:topic_id])
106+
topic = find_reviewable_commit_topic
107107

108108
State::CommitApproval.followup(topic, current_user)
109109

@@ -113,33 +113,15 @@ def followup
113113
def followed_up
114114
raise Discourse::InvalidAccess if !SiteSetting.code_review_allow_manual_followup
115115

116-
topic = Topic.find_by(id: params[:topic_id])
117-
118-
tags = topic.tags.pluck(:name)
119-
120-
if tags.include?(SiteSetting.code_review_followup_tag)
121-
tags -= [SiteSetting.code_review_approved_tag, SiteSetting.code_review_followup_tag]
122-
123-
tags << SiteSetting.code_review_pending_tag
124-
125-
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(current_user), tags)
126-
127-
topic.add_moderator_post(
128-
current_user,
129-
nil,
130-
bump: false,
131-
post_type: Post.types[:small_action],
132-
action_code: "followed_up",
133-
)
116+
topic = find_reviewable_commit_topic
134117

135-
DiscourseEvent.trigger(:unassign_topic, topic, current_user)
136-
end
118+
State::CommitApproval.complete_followup(topic, current_user)
137119

138120
render json: success_json
139121
end
140122

141123
def approve
142-
topic = Topic.find_by(id: params[:topic_id])
124+
topic = find_reviewable_commit_topic
143125

144126
if !SiteSetting.code_review_allow_self_approval && topic.user_id == current_user.id
145127
raise Discourse::InvalidAccess
@@ -174,6 +156,14 @@ def redirect
174156

175157
protected
176158

159+
def find_reviewable_commit_topic
160+
topic = Topic.find_by(id: params[:topic_id])
161+
guardian.ensure_can_see!(topic)
162+
raise Discourse::InvalidAccess if !topic.code_review_commit_topic
163+
164+
topic
165+
end
166+
177167
def render_next_topic(category_id)
178168
category_filter_sql = <<~SQL
179169
category_id NOT IN (

lib/discourse_code_review/state/commit_approval.rb

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ module DiscourseCodeReview::State::CommitApproval
66

77
class << self
88
def skip(topic, user)
9+
ensure_can_skip!(topic, user)
10+
911
if SiteSetting.code_review_skip_duration_minutes > 0
1012
DiscourseCodeReview::SkippedCodeReview.upsert(
1113
{
@@ -21,6 +23,8 @@ def skip(topic, user)
2123
end
2224

2325
def approve(topic, approvers, pr: nil, merged_by: nil)
26+
approvers.each { |approver| ensure_can_approve!(topic, approver) }
27+
2428
last_post = nil
2529
approvers.each { |approver| last_post = ensure_approved_post(topic, approver) }
2630

@@ -38,14 +42,14 @@ def approve(topic, approvers, pr: nil, merged_by: nil)
3842
end
3943

4044
def followup(topic, actor)
41-
tags = topic.tags.pluck(:name)
45+
tags = ensure_can_followup!(topic, actor)
4246

4347
if !tags.include?(SiteSetting.code_review_followup_tag)
4448
tags -= [SiteSetting.code_review_approved_tag, SiteSetting.code_review_pending_tag]
4549

4650
tags << SiteSetting.code_review_followup_tag
4751

48-
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(actor), tags)
52+
DiscourseTagging.tag_topic_by_names(topic, actor.guardian, tags)
4953

5054
topic.add_moderator_post(
5155
actor,
@@ -61,6 +65,25 @@ def followup(topic, actor)
6165
end
6266
end
6367

68+
def complete_followup(topic, actor)
69+
tags = ensure_can_complete_followup!(topic, actor)
70+
71+
tags -= [SiteSetting.code_review_approved_tag, SiteSetting.code_review_followup_tag]
72+
tags << SiteSetting.code_review_pending_tag
73+
74+
DiscourseTagging.tag_topic_by_names(topic, actor.guardian, tags)
75+
76+
topic.add_moderator_post(
77+
actor,
78+
nil,
79+
bump: false,
80+
post_type: Post.types[:small_action],
81+
action_code: "followed_up",
82+
)
83+
84+
DiscourseEvent.trigger(:unassign_topic, topic, actor)
85+
end
86+
6487
def followed_up(followee_topic, follower_topic)
6588
last_post =
6689
followee_topic.add_moderator_post(
@@ -87,6 +110,58 @@ def followed_up(followee_topic, follower_topic)
87110

88111
private
89112

113+
def ensure_can_skip!(topic, actor)
114+
tags = ensure_can_review_commit_topic!(topic, actor)
115+
raise Discourse::InvalidAccess if (tags & approvable_tags).empty?
116+
end
117+
118+
def ensure_can_approve!(topic, actor)
119+
tags = ensure_can_review_commit_topic!(topic, actor)
120+
if !tags.include?(SiteSetting.code_review_approved_tag) && (tags & approvable_tags).empty?
121+
raise Discourse::InvalidAccess
122+
end
123+
124+
if !SiteSetting.code_review_allow_self_approval && topic.user_id == actor.id
125+
raise Discourse::InvalidAccess
126+
end
127+
end
128+
129+
def ensure_can_followup!(topic, actor)
130+
tags = ensure_can_review_commit_topic!(topic, actor)
131+
if !tags.include?(SiteSetting.code_review_followup_tag) &&
132+
(
133+
tags & [SiteSetting.code_review_pending_tag, SiteSetting.code_review_approved_tag]
134+
).empty?
135+
raise Discourse::InvalidAccess
136+
end
137+
138+
tags
139+
end
140+
141+
def ensure_can_complete_followup!(topic, actor)
142+
tags = ensure_can_review_commit_topic!(topic, actor)
143+
raise Discourse::InvalidAccess if actor.id != topic.user_id && !actor.staff?
144+
raise Discourse::InvalidAccess if !tags.include?(SiteSetting.code_review_followup_tag)
145+
146+
tags
147+
end
148+
149+
def ensure_can_review_commit_topic!(topic, actor, require_code_reviewer: true)
150+
raise Discourse::InvalidAccess if !topic || !actor
151+
if require_code_reviewer && !actor.admin? && !actor.can_review_code?
152+
raise Discourse::InvalidAccess
153+
end
154+
155+
actor.guardian.ensure_can_see!(topic)
156+
raise Discourse::InvalidAccess if !topic.code_review_commit_topic
157+
158+
topic.tags.pluck(:name)
159+
end
160+
161+
def approvable_tags
162+
[SiteSetting.code_review_pending_tag, SiteSetting.code_review_followup_tag]
163+
end
164+
90165
def ensure_approved_post(topic, approver)
91166
DistributedMutex.synchronize("code-review:ensure-approved-post:#{topic.id}") do
92167
ActiveRecord::Base.transaction(requires_new: true) do
@@ -121,7 +196,7 @@ def transition_to_approved(topic)
121196

122197
tags << SiteSetting.code_review_approved_tag
123198

124-
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(Discourse.system_user), tags)
199+
DiscourseTagging.tag_topic_by_names(topic, Discourse.system_user.guardian, tags)
125200
already_approved = false
126201
end
127202
end

spec/discourse_code_review/lib/state/commit_approval_spec.rb

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
module DiscourseCodeReview
44
describe State::CommitApproval do
5-
fab!(:topic)
5+
fab!(:pending_tag) { Fabricate(:tag, name: SiteSetting.code_review_pending_tag) }
6+
fab!(:topic) { Fabricate(:topic, tags: [pending_tag]) }
67
fab!(:pr) do
78
DiscourseCodeReview::PullRequest.new(owner: "owner", name: "name", issue_number: 101)
89
end
9-
fab!(:approver, :user)
10-
fab!(:merged_by, :user)
10+
fab!(:approver, :admin)
11+
fab!(:merged_by, :admin)
12+
13+
before do
14+
DiscourseCodeReview::CommitTopic.create!(topic_id: topic.id, sha: SecureRandom.hex(20))
15+
end
1116

1217
describe "#ensure_pr_merge_info_post" do
1318
it "does not consider duplicate approvers" do
@@ -24,11 +29,48 @@ module DiscourseCodeReview
2429
end
2530
end
2631

32+
describe ".followup" do
33+
fab!(:reviewer_group, :group)
34+
fab!(:actor, :user)
35+
fab!(:private_group, :group)
36+
37+
fab!(:private_category) { Fabricate(:private_category, group: private_group) }
38+
39+
fab!(:private_topic) do
40+
Fabricate(:topic, category: private_category, tags: [pending_tag], user: Fabricate(:admin))
41+
end
42+
43+
before do
44+
SiteSetting.code_review_allowed_groups = reviewer_group.id.to_s
45+
SiteSetting.tagging_enabled = true
46+
reviewer_group.add(actor)
47+
DiscourseCodeReview::CommitTopic.create!(
48+
topic_id: private_topic.id,
49+
sha: SecureRandom.hex(20),
50+
)
51+
end
52+
53+
it "requires the actor to see the topic" do
54+
expect(actor.guardian.can_see?(private_topic)).to eq(false)
55+
56+
expect { State::CommitApproval.followup(private_topic, actor) }.to raise_error(
57+
Discourse::InvalidAccess,
58+
)
59+
expect(private_topic.reload.tags.pluck(:name)).to contain_exactly(
60+
SiteSetting.code_review_pending_tag,
61+
)
62+
end
63+
end
64+
2765
describe "creates notifications upon approval" do
2866
before { SiteSetting.code_review_enabled = true }
2967

3068
it "doesn't consolidate notifications if they were created more than 6 hours ago" do
31-
second_topic = Fabricate(:topic, user: topic.user)
69+
second_topic = Fabricate(:topic, tags: [pending_tag], user: topic.user)
70+
DiscourseCodeReview::CommitTopic.create!(
71+
topic_id: second_topic.id,
72+
sha: SecureRandom.hex(20),
73+
)
3274
second_pr =
3375
DiscourseCodeReview::PullRequest.new(owner: "owner", name: "name", issue_number: 102)
3476

0 commit comments

Comments
 (0)