0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/spec/lib/application_layout_preloader_spec.rb
Martin Brennan 7e77ce4bd3
DEV: Add resolve_group_membership functionality to theme settings (#41360)
Currently, theme settings with `type: list` and `list_type: group`
require client-side permission checks, but `currentUser.groups` only
includes visible groups, not all groups the user belongs to. This makes
permission checks unreliable and can leak hidden group membership.

To address this, this commit adds an opt-in `resolve_group_membership:
true` option that replaces the group ID list with a user_in_SETTING_NAME
boolean resolved server-side via `guardian.in_any_groups?`. The original
group list is removed from the frontend payload to prevent leaking group
IDs.

Since theme settings are cached per-theme (not per-user), the resolution
happens after the cache lookup during per-request serialization in
`ApplicationLayoutPreloader#activated_themes_json`.

Example YAML:

```yaml
copy_button_allowed_groups:
  type: list
  list_type: group
  resolve_group_membership: true
  default: "1|3"
```

Frontend usage:

```javascript
if (!settings.user_in_copy_button_allowed_groups) return;
```
2026-07-08 09:29:21 +10:00

165 lines
4.2 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe ApplicationLayoutPreloader do
subject do
described_class.new(
guardian: guardian,
theme_id: theme_id,
theme_target: :desktop,
login_method: nil,
)
end
let(:theme_id) { nil }
describe "#preloaded_data" do
let(:guardian) { user.guardian }
let(:preloaded_data) { subject.preloaded_data }
context "when the user is anonymous" do
let(:guardian) { Guardian.new }
it "preloads the anonymous keys" do
expect(preloaded_data.keys).to contain_exactly(
"site",
"siteSettings",
"themeSiteSettingOverrides",
"customHTML",
"banner",
"customEmoji",
"isReadOnly",
"isStaffWritesOnly",
"activatedThemes",
"upcomingChanges",
)
end
end
context "when the user is logged in" do
fab!(:user)
it "preloads the current user keys" do
expect(preloaded_data.keys).to contain_exactly(
"site",
"siteSettings",
"themeSiteSettingOverrides",
"customHTML",
"banner",
"customEmoji",
"isReadOnly",
"isStaffWritesOnly",
"activatedThemes",
"upcomingChanges",
"currentUser",
"topicTrackingStates",
"topicTrackingStateMeta",
)
end
end
context "when the user is an admin" do
fab!(:user, :admin)
it "preloads the admin keys" do
expect(preloaded_data.keys).to contain_exactly(
"site",
"siteSettings",
"themeSiteSettingOverrides",
"customHTML",
"banner",
"customEmoji",
"isReadOnly",
"isStaffWritesOnly",
"activatedThemes",
"upcomingChanges",
"currentUser",
"topicTrackingStates",
"topicTrackingStateMeta",
"fontMap",
"visiblePlugins",
)
end
end
end
describe "#activated_themes_json" do
fab!(:user)
fab!(:theme) { Fabricate(:theme, name: "Parent theme") }
fab!(:component) { Fabricate(:theme, name: "Theme component", component: true) }
fab!(:group)
fab!(:other_group, :group)
let(:guardian) { user.guardian }
let(:activated_themes) { JSON.parse(subject.preloaded_data["activatedThemes"]) }
before do
theme.set_field(target: :settings, name: :yaml, value: <<~YAML)
color:
default: "red"
allowed_groups:
type: list
list_type: group
resolve_group_membership: true
default: "#{group.id}"
other_allowed_groups:
type: list
list_type: group
resolve_group_membership: true
default: "#{other_group.id}"
YAML
component.set_field(target: :settings, name: :yaml, value: <<~YAML)
component_setting:
default: "enabled"
YAML
theme.save!
component.save!
group.add(user)
theme.add_relative_theme!(:child, component)
end
context "without a theme id" do
let(:theme_id) { nil }
it "returns an empty object" do
expect(activated_themes).to eq({})
end
end
context "with an active theme" do
let(:theme_id) { theme.id }
it "serializes the active theme and components" do
expect(activated_themes).to eq(
theme.id.to_s => {
"name" => "Parent theme",
"settings" => {
"color" => "red",
"user_in_allowed_groups" => true,
"user_in_other_allowed_groups" => false,
},
},
component.id.to_s => {
"name" => "Theme component",
"settings" => {
"component_setting" => "enabled",
},
},
)
end
end
context "with an anonymous user" do
let(:guardian) { Guardian.new }
let(:theme_id) { theme.id }
it "resolves group settings for the anonymous guardian" do
expect(activated_themes.dig(theme.id.to_s, "settings")).to include(
"user_in_allowed_groups" => false,
"user_in_other_allowed_groups" => false,
)
end
end
end
end