discourse/spec/lib/topic_creator_spec.rb
Alan Guo Xiang Tan 0b84353162
SECURITY: Prevent arbitrary topic custom fields from being set
Why this change?

The `PostsController#create` action allows arbitrary topic custom fields
to be set by any user that can create a topic. Without any restrictions,
this opens us up to potential security issues where plugins may be using
topic custom fields in security sensitive areas.

What does this change do?

1. This change introduces the `register_editable_topic_custom_field` plugin
API which allows plugins to register topic custom fields that are
editable either by staff users only or all users. The registered
editable topic custom fields are stored in `DiscoursePluginRegistry` and
is called by a new method `Topic#editable_custom_fields` which is then
used in the `PostsController#create` controller action. When an unpermitted custom fields is present in the `meta_data` params,
a 400 response code is returned.

2. Removes all reference to `meta_data` on a topic as it is confusing
   since we actually mean topic custom fields instead.
2023-10-16 10:51:28 -04:00

647 lines
22 KiB
Ruby

# frozen_string_literal: true
RSpec.describe TopicCreator do
fab!(:user) { Fabricate(:user, trust_level: TrustLevel[2]) }
fab!(:moderator) { Fabricate(:moderator) }
fab!(:admin) { Fabricate(:admin) }
let(:valid_attrs) { Fabricate.attributes_for(:topic) }
let(:pm_valid_attrs) do
{
raw: "this is a new post",
title: "this is a new title",
archetype: Archetype.private_message,
target_usernames: moderator.username,
}
end
let(:pm_to_email_valid_attrs) do
{
raw: "this is a new email",
title: "this is a new subject",
archetype: Archetype.private_message,
target_emails: "moderator@example.com",
}
end
describe "#create" do
context "with topic success cases" do
before do
TopicCreator.any_instance.expects(:save_topic).returns(true)
TopicCreator.any_instance.expects(:watch_topic).returns(true)
SiteSetting.allow_duplicate_topic_titles = true
end
it "should be possible for an admin to create a topic" do
expect(TopicCreator.create(admin, Guardian.new(admin), valid_attrs)).to be_valid
end
it "should be possible for a moderator to create a topic" do
expect(TopicCreator.create(moderator, Guardian.new(moderator), valid_attrs)).to be_valid
end
it "supports custom_fields that has been registered to the DiscoursePluginRegistry" do
opts = valid_attrs.merge(custom_fields: { import_id: "bar" })
topic = TopicCreator.create(admin, Guardian.new(admin), opts)
expect(topic.custom_fields["import_id"]).to eq("bar")
end
context "with regular user" do
before { SiteSetting.min_trust_to_create_topic = TrustLevel[0] }
it "should be possible for a regular user to create a topic" do
expect(TopicCreator.create(user, Guardian.new(user), valid_attrs)).to be_valid
end
it "should be possible for a regular user to create a topic with blank auto_close_time" do
expect(
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(auto_close_time: "")),
).to be_valid
end
it "ignores auto_close_time without raising an error" do
topic =
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(auto_close_time: "24"))
expect(topic).to be_valid
expect(topic.public_topic_timer).to eq(nil)
end
it "can create a topic in a category" do
category = Fabricate(:category, name: "Neil's Blog")
topic =
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(category: category.id))
expect(topic).to be_valid
expect(topic.category).to eq(category)
end
it "ignores participant_count without raising an error" do
topic =
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(participant_count: 3))
expect(topic.participant_count).to eq(1)
end
it "accepts participant_count in import mode" do
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(import_mode: true, participant_count: 3),
)
expect(topic.participant_count).to eq(3)
end
end
end
context "with tags" do
fab!(:tag1) { Fabricate(:tag, name: "fun") }
fab!(:tag2) { Fabricate(:tag, name: "fun2") }
fab!(:tag3) { Fabricate(:tag, name: "fun3") }
fab!(:tag4) { Fabricate(:tag, name: "fun4") }
fab!(:tag5) { Fabricate(:tag, name: "fun5") }
fab!(:tag_group1) { Fabricate(:tag_group, tags: [tag1]) }
fab!(:tag_group2) { Fabricate(:tag_group, tags: [tag2]) }
before do
SiteSetting.tagging_enabled = true
SiteSetting.min_trust_to_create_tag = 0
SiteSetting.min_trust_level_to_tag_topics = 0
end
context "with regular tags" do
it "user can add tags to topic" do
topic =
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(tags: [tag1.name]))
expect(topic).to be_valid
expect(topic.tags.length).to eq(1)
end
end
context "when assigned via matched watched words" do
fab!(:word1) do
Fabricate(:watched_word, action: WatchedWord.actions[:tag], replacement: tag1.name)
end
fab!(:word2) do
Fabricate(:watched_word, action: WatchedWord.actions[:tag], replacement: tag2.name)
end
fab!(:word3) do
Fabricate(
:watched_word,
action: WatchedWord.actions[:tag],
replacement: tag3.name,
case_sensitive: true,
)
end
it "adds watched words as tags" do
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(
title: "This is a #{word1.word} title",
raw: "#{word2.word.upcase} is not the same as #{word3.word.upcase}",
),
)
expect(topic).to be_valid
expect(topic.tags).to contain_exactly(tag1, tag2)
end
end
context "with staff-only tags" do
before { create_staff_only_tags(["alpha"]) }
it "regular users can't add staff-only tags" do
expect do
TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(tags: ["alpha"]))
end.to raise_error(ActiveRecord::Rollback)
end
it "staff can add staff-only tags" do
topic =
TopicCreator.create(admin, Guardian.new(admin), valid_attrs.merge(tags: ["alpha"]))
expect(topic).to be_valid
expect(topic.tags.length).to eq(1)
end
end
context "when minimum_required_tags is present" do
fab!(:category) { Fabricate(:category, name: "beta", minimum_required_tags: 2) }
it "fails for regular user if minimum_required_tags is not satisfied" do
expect(
TopicCreator.new(
user,
Guardian.new(user),
valid_attrs.merge(category: category.id),
).valid?,
).to be_falsy
end
it "lets admin create a topic regardless of minimum_required_tags" do
topic =
TopicCreator.create(
admin,
Guardian.new(admin),
valid_attrs.merge(tags: [tag1.name], category: category.id),
)
expect(topic).to be_valid
expect(topic.tags.length).to eq(1)
end
it "works for regular user if minimum_required_tags is satisfied" do
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(tags: [tag1.name, tag2.name], category: category.id),
)
expect(topic).to be_valid
expect(topic.tags.length).to eq(2)
end
it "minimum_required_tags is satisfying for new tags if user can create" do
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(tags: ["new tag", "another tag"], category: category.id),
)
expect(topic).to be_valid
expect(topic.tags.length).to eq(2)
end
it "lets new user create a topic if they don't have sufficient trust level to tag topics" do
SiteSetting.min_trust_level_to_tag_topics = 1
new_user = Fabricate(:newuser)
topic =
TopicCreator.create(
new_user,
Guardian.new(new_user),
valid_attrs.merge(category: category.id),
)
expect(topic).to be_valid
end
end
context "with required tag group" do
fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1]) }
fab!(:category) do
Fabricate(
:category,
name: "beta",
category_required_tag_groups: [
CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1),
],
)
end
it "when no tags are not present" do
expect(
TopicCreator.new(
user,
Guardian.new(user),
valid_attrs.merge(category: category.id),
).valid?,
).to be_falsy
end
it "when tags are not part of the tag group" do
expect(
TopicCreator.new(
user,
Guardian.new(user),
valid_attrs.merge(category: category.id, tags: ["nope"]),
).valid?,
).to be_falsy
end
it "when requirement is met" do
expect(
TopicCreator.new(
user,
Guardian.new(user),
valid_attrs.merge(category: category.id, tags: [tag1.name, tag2.name]),
).valid?,
).to be_truthy
end
it "lets staff ignore the restriction" do
expect(
TopicCreator.new(
user,
Guardian.new(admin),
valid_attrs.merge(category: category.id),
).valid?,
).to be_truthy
end
end
context "when category has restricted tags or tag groups" do
fab!(:category) { Fabricate(:category, tags: [tag3], tag_groups: [tag_group1]) }
it "allows topics without any tags" do
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic without tags",
raw: "hello this is a test topic without tags",
category: category.id,
)
expect(tc.valid?).to eq(true)
expect(tc.errors).to be_empty
topic = tc.create
expect(topic.tags).to be_empty
end
it "allows topics if they use tags only from the tags set that the category restricts" do
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag3.name],
)
expect(tc.valid?).to eq(true)
expect(tc.errors).to be_empty
topic = tc.create
expect(topic.tags).to contain_exactly(tag1, tag3)
end
it "allows topics to use tags that are restricted in multiple categories" do
category2 = Fabricate(:category, tags: [tag5], tag_groups: [tag_group1])
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category2.id,
tags: [tag1.name, tag5.name],
)
expect(tc.valid?).to eq(true)
expect(tc.errors).to be_empty
topic = tc.create
expect(topic.tags).to contain_exactly(tag1, tag5)
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags 1",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag3.name],
)
expect(tc.valid?).to eq(true)
expect(tc.errors).to be_empty
topic = tc.create
expect(topic.tags).to contain_exactly(tag1, tag3)
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags 2",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag5.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
count: 1,
tags: tag5.name,
category: category.name,
),
)
end
it "rejects topics if they use a tag outside the set of tags that the category restricts" do
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag2.name, tag1.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.category_does_not_allow_tags",
count: 1,
tags: tag2.name,
category: category.name,
),
)
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag2.name, tag5.name, tag3.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.category_does_not_allow_tags",
count: 2,
tags: [tag2, tag5].map(&:name).sort.join(", "),
category: category.name,
),
)
end
it "rejects topics in other categories if a restricted tag of a category are used" do
category2 = Fabricate(:category)
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category2.id,
tags: [tag1.name, tag2.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
count: 1,
tags: tag1.name,
category: category2.name,
),
)
end
context "when allowing other tags" do
before { category.update!(allow_global_tags: true) }
it "allows topics to use tags that aren't restricted by any category" do
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag2.name, tag3.name, tag5.name],
)
expect(tc.valid?).to eq(true)
expect(tc.errors).to be_empty
topic = tc.create
expect(topic.tags).to contain_exactly(tag1, tag2, tag3, tag5)
end
it "rejects topics if they use restricted tags of another category" do
Fabricate(:category, tags: [tag5], tag_groups: [tag_group2])
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag5.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
count: 1,
tags: tag5.name,
category: category.name,
),
)
tc =
TopicCreator.new(
user,
Guardian.new(user),
title: "hello this is a test topic with tags",
raw: "hello this is a test topic with tags",
category: category.id,
tags: [tag1.name, tag2.name, tag5.name],
)
expect(tc.valid?).to eq(false)
expect(tc.errors.full_messages).to contain_exactly(
I18n.t(
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
count: 2,
tags: [tag2, tag5].map(&:name).sort.join(", "),
category: category.name,
),
)
end
end
end
end
context "with personal message" do
context "with success cases" do
before do
TopicCreator.any_instance.expects(:save_topic).returns(true)
TopicCreator.any_instance.expects(:watch_topic).returns(true)
SiteSetting.allow_duplicate_topic_titles = true
SiteSetting.enable_staged_users = true
Group.refresh_automatic_groups!
end
it "should be possible for a regular user to send private message" do
expect(TopicCreator.create(user, Guardian.new(user), pm_valid_attrs)).to be_valid
end
it "min_trust_to_create_topic setting should not be checked when sending private message" do
SiteSetting.min_trust_to_create_topic = TrustLevel[4]
expect(TopicCreator.create(user, Guardian.new(user), pm_valid_attrs)).to be_valid
end
it "personal_message_enabled_groups setting should not be checked when sending private messages to staff via flag" do
SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:staff]
expect(
TopicCreator.create(
user,
Guardian.new(user),
pm_valid_attrs.merge(subtype: TopicSubtype.notify_moderators),
),
).to be_valid
end
end
context "with failure cases" do
it "should be rollback the changes when email is invalid" do
SiteSetting.manual_polling_enabled = true
SiteSetting.reply_by_email_address = "sam+%{reply_key}@sam.com"
SiteSetting.reply_by_email_enabled = true
SiteSetting.min_trust_to_send_email_messages = TrustLevel[1]
attrs = pm_to_email_valid_attrs.dup
attrs[:target_emails] = "t" * 256
expect do TopicCreator.create(user, Guardian.new(user), attrs) end.to raise_error(
ActiveRecord::Rollback,
)
end
it "personal_message_enabled_groups setting should be checked when sending private message" do
SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:trust_level_4]
expect do
TopicCreator.create(user, Guardian.new(user), pm_valid_attrs)
end.to raise_error(ActiveRecord::Rollback)
end
end
context "with to emails" do
before { Group.refresh_automatic_groups! }
it "works for staff" do
SiteSetting.min_trust_to_send_email_messages = "staff"
expect(
TopicCreator.create(admin, Guardian.new(admin), pm_to_email_valid_attrs),
).to be_valid
end
it "work for trusted users" do
SiteSetting.min_trust_to_send_email_messages = 3
user.update!(trust_level: 3)
expect(TopicCreator.create(user, Guardian.new(user), pm_to_email_valid_attrs)).to be_valid
end
it "does not work for non-staff" do
SiteSetting.min_trust_to_send_email_messages = "staff"
expect {
TopicCreator.create(user, Guardian.new(user), pm_to_email_valid_attrs)
}.to raise_error(ActiveRecord::Rollback)
end
it "does not work for untrusted users" do
SiteSetting.min_trust_to_send_email_messages = 3
user.update!(trust_level: 2)
expect {
TopicCreator.create(user, Guardian.new(user), pm_to_email_valid_attrs)
}.to raise_error(ActiveRecord::Rollback)
end
end
end
context "when setting timestamps" do
it "supports Time instances" do
freeze_time
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(created_at: 1.week.ago, pinned_at: 3.days.ago),
)
expect(topic.created_at).to eq_time(1.week.ago)
expect(topic.pinned_at).to eq_time(3.days.ago)
end
it "supports strings" do
freeze_time
time1 = Time.zone.parse("2019-09-02")
time2 = Time.zone.parse("2020-03-10 15:17")
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(created_at: "2019-09-02", pinned_at: "2020-03-10 15:17"),
)
expect(topic.created_at).to eq_time(time1)
expect(topic.pinned_at).to eq_time(time2)
end
end
context "with external_id" do
it "adds external_id" do
topic =
TopicCreator.create(
user,
Guardian.new(user),
valid_attrs.merge(external_id: "external_id"),
)
expect(topic.external_id).to eq("external_id")
end
end
context "when invisible/unlisted" do
let(:unlisted_attrs) { valid_attrs.merge(visible: false) }
it "throws an exception for a non-staff user" do
expect do TopicCreator.create(user, Guardian.new(user), unlisted_attrs) end.to raise_error(
ActiveRecord::Rollback,
)
end
it "is invalid for a non-staff user" do
expect(TopicCreator.new(user, Guardian.new(user), unlisted_attrs).valid?).to eq(false)
end
it "creates unlisted topic for an admin" do
expect(TopicCreator.create(admin, Guardian.new(admin), unlisted_attrs)).to be_valid
end
it "is valid for an admin" do
expect(TopicCreator.new(admin, Guardian.new(admin), unlisted_attrs).valid?).to eq(true)
end
end
end
end