From ee084b754eb5906cef515957627377e254186085 Mon Sep 17 00:00:00 2001 From: Kelvin Tan Date: Sun, 1 Oct 2023 23:10:35 +0800 Subject: [PATCH] SECURITY: Prevent unauthorized access to grouped poll results This adds access controls for the `/polls/grouped_poll_results` endpoint, such that only users with appropriate permissions can read the grouped results of a given poll. --- plugins/poll/lib/poll.rb | 12 +- .../spec/integration/poll_endpoints_spec.rb | 117 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/plugins/poll/lib/poll.rb b/plugins/poll/lib/poll.rb index e29564a7eb7..37c370c2b5b 100644 --- a/plugins/poll/lib/poll.rb +++ b/plugins/poll/lib/poll.rb @@ -198,11 +198,19 @@ class DiscoursePoll::Poll def self.grouped_poll_results(user, post_id, poll_name, user_field_name) raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists? - poll = - Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name) + Poll.includes(:poll_options, :poll_votes, post: :topic).find_by( + post_id: post_id, + name: poll_name, + ) raise Discourse::InvalidParameters.new(:poll_name) unless poll + # user must be allowed to post in topic + guardian = Guardian.new(user) + if !guardian.can_create_post?(poll.post.topic) + raise DiscoursePoll::Error.new I18n.t("poll.user_cant_post_in_topic") + end + unless SiteSetting.poll_groupable_user_fields.split("|").include?(user_field_name) raise Discourse::InvalidParameters.new(:user_field_name) end diff --git a/plugins/poll/spec/integration/poll_endpoints_spec.rb b/plugins/poll/spec/integration/poll_endpoints_spec.rb index e8b62dae9f9..0322752445c 100644 --- a/plugins/poll/spec/integration/poll_endpoints_spec.rb +++ b/plugins/poll/spec/integration/poll_endpoints_spec.rb @@ -147,6 +147,7 @@ RSpec.describe "DiscoursePoll endpoints" do let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" } before do + sign_in(user1) user_votes = { user_0: option_a, user_1: option_a, user_2: option_b } [user1, user2, user3].each_with_index do |user, index| @@ -219,5 +220,121 @@ RSpec.describe "DiscoursePoll endpoints" do expect(response.status).to eq(400) expect(response.body).to include("user_field_name") end + + context "when topic is in a private category" do + fab!(:admin) { Fabricate(:admin) } + fab!(:group) { Fabricate(:group) } + fab!(:private_category) { Fabricate(:private_category, group: group) } + fab!(:private_topic) { Fabricate(:topic, category: private_category) } + fab!(:private_post) { Fabricate(:post, topic: private_topic, raw: <<~SQL) } + [poll type=multiple public=true min=1 max=2] + - A + - B + [/poll] + SQL + let(:groupable_user_field) { "anything" } + let(:expected_results) do + { + grouped_results: [ + { + group: "Value0", + options: [ + { digest: option_a, html: "A", votes: 1 }, + { digest: option_b, html: "B", votes: 0 }, + ], + }, + { + group: "Value1", + options: [ + { digest: option_a, html: "A", votes: 2 }, + { digest: option_b, html: "B", votes: 1 }, + ], + }, + { + group: "Value2", + options: [ + { digest: option_a, html: "A", votes: 0 }, + { digest: option_b, html: "B", votes: 1 }, + ], + }, + ], + } + end + + before do + user_votes = { user_0: option_a, user_1: option_a, user_2: option_b } + SiteSetting.poll_groupable_user_fields = groupable_user_field + + [user1, user2, user3].each_with_index do |user, index| + group.add(user) + DiscoursePoll::Poll.vote( + user, + private_post.id, + DiscoursePoll::DEFAULT_POLL_NAME, + [user_votes["user_#{index}".to_sym]], + ) + UserCustomField.create( + user_id: user.id, + name: groupable_user_field, + value: "value#{index}", + ) + end + + # Add another user to one of the fields to prove it groups users properly + group.add(user4) + DiscoursePoll::Poll.vote( + user4, + private_post.id, + DiscoursePoll::DEFAULT_POLL_NAME, + [option_a, option_b], + ) + UserCustomField.create(user_id: user4.id, name: groupable_user_field, value: "value1") + end + + it "returns grouped poll results for admin based on user field" do + sign_in(admin) + + get "/polls/grouped_poll_results.json", + params: { + post_id: private_post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + user_field_name: groupable_user_field, + } + + expect(response).to have_http_status :success + expect(response.parsed_body.deep_symbolize_keys).to eq(expected_results) + end + + it "returns grouped poll results for user within private group based on user field" do + user = Fabricate(:user) + group.add(user) + sign_in(user) + + get "/polls/grouped_poll_results.json", + params: { + post_id: private_post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + user_field_name: groupable_user_field, + } + + expect(response).to have_http_status :success + expect(response.parsed_body.deep_symbolize_keys).to eq(expected_results) + end + + it "returns an error when user does not have access to topic category" do + user = Fabricate(:user) + sign_in(user) + + get "/polls/grouped_poll_results.json", + params: { + post_id: private_post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + user_field_name: groupable_user_field, + } + + expect(response).to have_http_status :unprocessable_entity + expect(response.parsed_body["errors"][0]).to eq(I18n.t("poll.user_cant_post_in_topic")) + end + end end end