SECURITY: Default tags to show count of topics in unrestricted categories (#19916)

Currently, `Tag#topic_count` is a count of all regular topics regardless of whether the topic is in a read restricted category or not. As a result, any users can technically poll a sensitive tag to determine if a new topic is created in a category which the user has not excess to. We classify this as a minor leak in sensitive information.

The following changes are introduced in this commit:

1. Introduce `Tag#public_topic_count` which only count topics which have been tagged with a given tag in public categories.
2. Rename `Tag#topic_count` to `Tag#staff_topic_count` which counts the same way as `Tag#topic_count`. In other words, it counts all topics tagged with a given tag regardless of the category the topic is in. The rename is also done so that we indicate that this column contains sensitive information. 
3. Change all previous spots which relied on `Topic#topic_count` to rely on `Tag.topic_column_count(guardian)` which will return the right "topic count" column to use based on the current scope. 
4. Introduce `SiteSetting.include_secure_categories_in_tag_counts` site setting to allow site administrators to always display the tag topics count using `Tag#staff_topic_count` instead.
This commit is contained in:
Alan Guo Xiang Tan
2023-01-20 09:50:24 +08:00
committed by GitHub
parent 4d2a95ffe6
commit f122f24b35
28 changed files with 602 additions and 127 deletions

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
RSpec.describe "Updating tag counts" do
fab!(:tag1) { Fabricate(:tag) }
fab!(:tag2) { Fabricate(:tag) }
fab!(:group) { Fabricate(:group) }
fab!(:public_category) { Fabricate(:category) }
fab!(:public_category2) { Fabricate(:category) }
fab!(:private_category) { Fabricate(:private_category, group: group) }
fab!(:private_category2) { Fabricate(:private_category, group: group) }
fab!(:topic_in_public_category) do
Fabricate(:topic, category: public_category, tags: [tag1, tag2]).tap do |topic|
Fabricate(:post, topic: topic)
end
end
fab!(:topic_in_private_category) do
Fabricate(:topic, category: private_category, tags: [tag1, tag2]).tap do |topic|
Fabricate(:post, topic: topic)
end
end
fab!(:private_message) do
topic = Fabricate(:private_message_post).topic
topic.update!(tags: [tag1, tag2])
topic
end
before do
expect(tag1.public_topic_count).to eq(1)
expect(tag1.staff_topic_count).to eq(2)
expect(tag1.pm_topic_count).to eq(1)
expect(tag2.reload.public_topic_count).to eq(1)
expect(tag2.staff_topic_count).to eq(2)
expect(tag2.pm_topic_count).to eq(1)
end
it "should decrease Tag#public_topic_count for all tags when topic's category is changed from a public category to a read restricted category" do
expect { topic_in_public_category.change_category_to_id(private_category.id) }.to change {
tag1.reload.public_topic_count
}.by(-1).and change { tag2.reload.public_topic_count }.by(-1)
end
it "should increase Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to a public category" do
expect { topic_in_private_category.change_category_to_id(public_category.id) }.to change {
tag1.reload.public_topic_count
}.by(1).and change { tag2.reload.public_topic_count }.by(1)
end
it "should not change Tag#public_topic_count for all tags when topic's category is changed from a public category to another public category" do
expect do
topic_in_public_category.change_category_to_id(public_category2.id)
end.to not_change { tag1.reload.public_topic_count }.and not_change {
tag2.reload.public_topic_count
}
end
it "should not change Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to another read restricted category" do
expect do
topic_in_private_category.change_category_to_id(private_category2.id)
end.to not_change { tag1.reload.public_topic_count }.and not_change {
tag2.reload.public_topic_count
}
end
it "increases Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a public category" do
expect do
private_message.convert_to_public_topic(
Discourse.system_user,
category_id: public_category.id,
)
end.to change { tag1.reload.public_topic_count }.by(1).and change {
tag2.reload.public_topic_count
}.by(1)
end
it "should not change Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a read restricted category" do
expect do
private_message.convert_to_public_topic(
Discourse.system_user,
category_id: private_category.id,
)
end.to not_change { tag1.reload.public_topic_count }.and not_change {
tag2.reload.public_topic_count
}
end
it "should decrease Tag#public_topic_count for all tags when regular topic in public category is converted to a private message" do
expect do
topic_in_public_category.convert_to_private_message(Discourse.system_user)
end.to change { tag1.reload.public_topic_count }.by(-1).and change {
tag2.reload.public_topic_count
}.by(-1)
end
it "should not change Tag#public_topic_count for all tags when regular topic in read restricted category is converted to a private message" do
expect do
topic_in_private_category.convert_to_private_message(Discourse.system_user)
end.to not_change { tag1.reload.public_topic_count }.and not_change {
tag2.reload.public_topic_count
}
end
end