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

@ -202,29 +202,101 @@ RSpec.describe Tag do
end
end
describe "topic counts" do
describe ".ensure_consistency!" do
it "should exclude private message topics" do
topic
Fabricate(:private_message_topic, tags: [tag])
Tag.ensure_consistency!
tag.reload
expect(tag.topic_count).to eq(1)
expect(tag.staff_topic_count).to eq(1)
expect(tag.public_topic_count).to eq(1)
end
it "should update Tag#topic_count and Tag#public_topic_count correctly" do
tag = Fabricate(:tag, name: "tag1")
tag2 = Fabricate(:tag, name: "tag2")
tag3 = Fabricate(:tag, name: "tag3")
group = Fabricate(:group)
category = Fabricate(:category)
private_category = Fabricate(:private_category, group: group)
private_category2 = Fabricate(:private_category, group: group)
_topic_with_tag = Fabricate(:topic, category: category, tags: [tag])
_topic_with_tag_in_private_category =
Fabricate(:topic, category: private_category, tags: [tag])
_topic_with_tag2_in_private_category2 =
Fabricate(:topic, category: private_category2, tags: [tag2])
tag.update!(staff_topic_count: 123, public_topic_count: 456)
tag2.update!(staff_topic_count: 123, public_topic_count: 456)
tag3.update!(staff_topic_count: 123, public_topic_count: 456)
Tag.ensure_consistency!
tag.reload
tag2.reload
tag3.reload
expect(tag.staff_topic_count).to eq(2)
expect(tag.public_topic_count).to eq(1)
expect(tag2.staff_topic_count).to eq(1)
expect(tag2.public_topic_count).to eq(0)
expect(tag3.staff_topic_count).to eq(0)
expect(tag3.public_topic_count).to eq(0)
end
end
describe "unused tags scope" do
let!(:tags) do
[
Fabricate(:tag, name: "used_publically", topic_count: 2, pm_topic_count: 0),
Fabricate(:tag, name: "used_privately", topic_count: 0, pm_topic_count: 3),
Fabricate(:tag, name: "used_everywhere", topic_count: 0, pm_topic_count: 3),
Fabricate(:tag, name: "unused1", topic_count: 0, pm_topic_count: 0),
Fabricate(:tag, name: "unused2", topic_count: 0, pm_topic_count: 0),
Fabricate(
:tag,
name: "used_publically",
staff_topic_count: 2,
public_topic_count: 2,
pm_topic_count: 0,
),
Fabricate(
:tag,
name: "used_privately",
staff_topic_count: 0,
public_topic_count: 0,
pm_topic_count: 3,
),
Fabricate(
:tag,
name: "used_everywhere",
staff_topic_count: 0,
public_topic_count: 0,
pm_topic_count: 3,
),
Fabricate(
:tag,
name: "unused1",
staff_topic_count: 0,
public_topic_count: 0,
pm_topic_count: 0,
),
Fabricate(
:tag,
name: "unused2",
staff_topic_count: 0,
public_topic_count: 0,
pm_topic_count: 0,
),
]
end
let(:tag_in_group) do
Fabricate(:tag, name: "unused_in_group", topic_count: 0, pm_topic_count: 0)
Fabricate(
:tag,
name: "unused_in_group",
public_topic_count: 0,
staff_topic_count: 0,
pm_topic_count: 0,
)
end
let!(:tag_group) { Fabricate(:tag_group, tag_names: [tag_in_group.name]) }
@ -292,4 +364,22 @@ RSpec.describe Tag do
expect(category.reload.tags).to include(tag2)
end
end
describe ".topic_count_column" do
fab!(:admin) { Fabricate(:admin) }
it "returns 'staff_topic_count' when user is staff" do
expect(Tag.topic_count_column(Guardian.new(admin))).to eq("staff_topic_count")
end
it "returns 'public_topic_count' when user is not staff" do
expect(Tag.topic_count_column(Guardian.new(user))).to eq("public_topic_count")
end
it "returns 'staff_topic_count' when user is not staff but `include_secure_categories_in_tag_counts` site setting is enabled" do
SiteSetting.include_secure_categories_in_tag_counts = true
expect(Tag.topic_count_column(Guardian.new(user))).to eq("staff_topic_count")
end
end
end

View File

@ -1,33 +1,56 @@
# frozen_string_literal: true
RSpec.describe TopicTag do
fab!(:group) { Fabricate(:group) }
fab!(:private_category) { Fabricate(:private_category, group: group) }
fab!(:topic) { Fabricate(:topic) }
fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category) }
fab!(:tag) { Fabricate(:tag) }
let(:topic_tag) { Fabricate(:topic_tag, topic: topic, tag: tag) }
describe "#after_create" do
it "tag topic_count should be increased" do
expect { topic_tag }.to change(tag, :topic_count).by(1)
it "should increase Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do
expect { topic_tag }.to change { tag.reload.staff_topic_count }.by(1).and change {
tag.reload.public_topic_count
}.by(1)
end
it "tag topic_count should not be increased" do
it "should only increase Tag#staff_topic_count for a regular topic in a read restricted category" do
expect { Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag) }.to change {
tag.reload.staff_topic_count
}.by(1)
expect(tag.reload.public_topic_count).to eq(0)
end
it "should increase Tag#pm_topic_count for a private message topic" do
topic.archetype = Archetype.private_message
expect { topic_tag }.not_to change(tag, :topic_count)
expect { topic_tag }.to change { tag.reload.pm_topic_count }.by(1)
end
end
describe "#after_destroy" do
it "tag topic_count should be decreased" do
it "should decrease Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do
topic_tag
expect { topic_tag.destroy }.to change(tag, :topic_count).by(-1)
expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1).and change {
tag.reload.public_topic_count
}.by(-1)
end
it "tag topic_count should not be decreased" do
it "should only decrease Topic#topic_count for a regular topic in a read restricted category" do
topic_tag = Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag)
expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1)
expect(tag.reload.public_topic_count).to eq(0)
end
it "should decrease Tag#pm_topic_count for a private message topic" do
topic.archetype = Archetype.private_message
topic_tag
expect { topic_tag.destroy }.not_to change(tag, :topic_count)
expect { topic_tag.destroy! }.to change { tag.reload.pm_topic_count }.by(-1)
end
end
end