0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/lib/theme_settings_manager_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

230 lines
6.8 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe ThemeSettingsManager do
let!(:theme) { Fabricate(:theme) }
let(:theme_settings) do
yaml = File.read("#{Rails.root.join("spec/fixtures/theme_settings/valid_settings.yaml")}")
theme.set_field(target: :settings, name: "yaml", value: yaml)
theme.save!
theme.settings
end
describe "Enum" do
it "only accepts values from its choices" do
enum_setting = theme_settings[:enum_setting]
expect { enum_setting.value = "trust level 2" }.to raise_error(Discourse::InvalidParameters)
expect { enum_setting.value = "trust level 0" }.not_to raise_error
enum_setting = theme_settings[:enum_setting_02]
expect { enum_setting.value = "10" }.not_to raise_error
enum_setting = theme_settings[:enum_setting_03]
expect { enum_setting.value = "10" }.not_to raise_error
expect { enum_setting.value = 1 }.not_to raise_error
expect { enum_setting.value = 15 }.to raise_error(Discourse::InvalidParameters)
end
end
describe "Bool" do
it "is either true or false" do
bool_setting = theme_settings[:boolean_setting]
expect(bool_setting.value).to eq(true) # default
bool_setting.value = "true"
theme.reload
expect(bool_setting.value).to eq(true)
bool_setting.value = "falsse" # intentionally misspelled
theme.reload
expect(bool_setting.value).to eq(false)
bool_setting.value = true
theme.reload
expect(bool_setting.value).to eq(true)
end
end
describe "Integer" do
it "is always an integer" do
int_setting = theme_settings[:integer_setting]
int_setting.value = 1.6
theme.reload
expect(int_setting.value).to eq(1)
int_setting.value = "4.3"
theme.reload
expect(int_setting.value).to eq(4)
int_setting.value = "10"
theme.reload
expect(int_setting.value).to eq(10)
int_setting.value = "text"
theme.reload
expect(int_setting.value).to eq(0)
end
it "can have min or max value" do
int_setting = theme_settings[:integer_setting_02]
expect { int_setting.value = 0 }.to raise_error(Discourse::InvalidParameters)
expect { int_setting.value = 61 }.to raise_error(Discourse::InvalidParameters)
int_setting.value = 60
theme.reload
expect(int_setting.value).to eq(60)
int_setting.value = 1
theme.reload
expect(int_setting.value).to eq(1)
end
end
describe "Float" do
it "is always a float" do
float_setting = theme_settings[:float_setting]
float_setting.value = 1.615
theme.reload
expect(float_setting.value).to eq(1.615)
float_setting.value = "3.1415"
theme.reload
expect(float_setting.value).to eq(3.1415)
float_setting.value = 10
theme.reload
expect(float_setting.value).to eq(10)
end
it "can have min or max value" do
float_setting = theme_settings[:float_setting]
expect { float_setting.value = 1.4 }.to raise_error(Discourse::InvalidParameters)
expect { float_setting.value = 10.01 }.to raise_error(Discourse::InvalidParameters)
expect { float_setting.value = "text" }.to raise_error(Discourse::InvalidParameters)
float_setting.value = 9.521
theme.reload
expect(float_setting.value).to eq(9.521)
end
end
describe "String" do
it "can have min or max length" do
string_setting = theme_settings[:string_setting_02]
expect { string_setting.value = "a" }.to raise_error(Discourse::InvalidParameters)
string_setting.value = "ab"
theme.reload
expect(string_setting.value).to eq("ab")
string_setting.value = "ab" * 10
theme.reload
expect(string_setting.value).to eq("ab" * 10)
expect { string_setting.value = ("a" * 21) }.to raise_error(Discourse::InvalidParameters)
end
it "can be a textarea" do
expect(theme_settings[:string_setting_02].textarea).to eq(false)
expect(theme_settings[:string_setting_03].textarea).to eq(true)
end
it "supports json schema" do
expect(theme_settings[:string_setting_03].json_schema).to eq(false)
expect(theme_settings[:invalid_json_schema_setting].json_schema).to eq(false)
expect(theme_settings[:valid_json_schema_setting].json_schema).to be_truthy
end
end
describe "List" do
it "can have a list type" do
list_setting = theme_settings[:compact_list_setting]
expect(list_setting.list_type).to eq("compact")
end
describe "#resolve_group_membership?" do
it "returns true when opted-in with list_type group" do
yaml = <<~YAML
test_setting:
type: list
list_type: group
resolve_group_membership: true
default: "1|2"
YAML
theme.set_field(target: :settings, name: "yaml", value: yaml)
theme.save!
setting = theme.settings[:test_setting]
expect(setting.resolve_group_membership?).to eq(true)
end
it "returns false when list_type is not group" do
setting =
described_class.create(
:test_setting,
"a|b",
ThemeSetting.types[:list],
theme,
list_type: "compact",
resolve_group_membership: true,
)
expect(setting.resolve_group_membership?).to eq(false)
end
it "returns false when not opted-in" do
yaml = <<~YAML
test_setting:
type: list
list_type: group
default: "1|2"
YAML
theme.set_field(target: :settings, name: "yaml", value: yaml)
theme.save!
setting = theme.settings[:test_setting]
expect(setting.resolve_group_membership?).to eq(false)
end
end
end
describe "Upload" do
let!(:upload) { Fabricate(:upload) }
it "saves the upload id" do
upload_setting = theme_settings[:upload_setting]
upload_setting.value = upload.url
theme.reload
expect(
ThemeSetting.exists?(theme_id: theme.id, name: "upload_setting", value: upload.id.to_s),
).to be_truthy
end
describe "#value" do
context "when it's changed to a custom upload" do
it "returns CDN URL" do
upload_setting = theme_settings[:upload_setting]
upload_setting.value = upload.url
theme.reload
expect(upload_setting.value).to eq(Discourse.store.cdn_url(upload.url))
end
end
context "when there's a default upload" do
it "returns CDN URL" do
theme.set_field(
target: :common,
name: "default-upload",
type: :theme_upload_var,
upload_id: upload.id,
)
theme.save!
upload_setting = theme_settings[:upload_setting]
expect(upload_setting.value).to eq(Discourse.store.cdn_url(upload.url))
end
end
end
end
end