mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
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; ```
130 lines
4.1 KiB
Ruby
Vendored
130 lines
4.1 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "theme_settings_parser"
|
|
|
|
RSpec.describe ThemeSettingsParser do
|
|
after { ThemeField.destroy_all }
|
|
|
|
def types
|
|
ThemeSetting.types
|
|
end
|
|
|
|
class Loader
|
|
def initialize
|
|
@settings = []
|
|
load_settings
|
|
end
|
|
|
|
def load_settings
|
|
yaml = File.read("#{Rails.root.join("spec/fixtures/theme_settings/valid_settings.yaml")}")
|
|
field = ThemeField.create!(theme_id: -1, target_id: 3, name: "yaml", value: yaml)
|
|
|
|
ThemeSettingsParser
|
|
.new(field)
|
|
.load { |name, default, type, opts| @settings << setting(name, default, type, opts) }
|
|
end
|
|
|
|
def setting(name, default, type, opts = {})
|
|
{ name: name, default: default, type: type, opts: opts }
|
|
end
|
|
|
|
def find_by_name(name)
|
|
@settings.find { |setting| setting[:name] == name }
|
|
end
|
|
end
|
|
|
|
let(:loader) { Loader.new }
|
|
|
|
it "guesses types correctly" do
|
|
expect(loader.find_by_name(:boolean_setting)[:type]).to eq(types[:bool])
|
|
expect(loader.find_by_name(:boolean_setting_02)[:type]).to eq(types[:bool])
|
|
expect(loader.find_by_name(:string_setting)[:type]).to eq(types[:string])
|
|
expect(loader.find_by_name(:integer_setting)[:type]).to eq(types[:integer])
|
|
expect(loader.find_by_name(:integer_setting_03)[:type]).to eq(types[:integer])
|
|
expect(loader.find_by_name(:float_setting)[:type]).to eq(types[:float])
|
|
expect(loader.find_by_name(:list_setting)[:type]).to eq(types[:list])
|
|
expect(loader.find_by_name(:enum_setting)[:type]).to eq(types[:enum])
|
|
end
|
|
|
|
describe "description locale" do
|
|
it "favors I18n.locale" do
|
|
I18n.locale = :ar
|
|
SiteSetting.default_locale = "en"
|
|
expect(loader.find_by_name(:enum_setting_02)[:opts][:description]).to eq("Arabic text")
|
|
end
|
|
|
|
it "uses SiteSetting.default_locale if I18n.locale isn't supported" do
|
|
I18n.locale = :en
|
|
SiteSetting.default_locale = "es"
|
|
expect(loader.find_by_name(:integer_setting_02)[:opts][:description]).to eq("Spanish text")
|
|
end
|
|
|
|
it "finds the first supported locale and uses it as a last resort" do
|
|
I18n.locale = :de
|
|
SiteSetting.default_locale = "it"
|
|
expect(loader.find_by_name(:integer_setting_02)[:opts][:description]).to eq("French text")
|
|
end
|
|
|
|
it "doesn't set locale if no supported locale is provided" do
|
|
expect(loader.find_by_name(:integer_setting_03)[:opts][:description]).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "enum setting" do
|
|
it "should never have less than 1 choices" do
|
|
choices = loader.find_by_name(:enum_setting)[:opts][:choices]
|
|
expect(choices.class).to eq(Array)
|
|
expect(choices.length).to eq(3)
|
|
|
|
choices = loader.find_by_name(:enum_setting_02)[:opts][:choices]
|
|
expect(choices.class).to eq(Array)
|
|
expect(choices.length).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe "list setting" do
|
|
it "supports list type" do
|
|
list_type = loader.find_by_name(:compact_list_setting)[:opts][:list_type]
|
|
expect(list_type).to eq("compact")
|
|
end
|
|
end
|
|
|
|
describe "resolve_group_membership" do
|
|
it "parses resolve_group_membership option" do
|
|
yaml = <<~YAML
|
|
groups_setting:
|
|
type: list
|
|
list_type: group
|
|
resolve_group_membership: true
|
|
default: "1|2"
|
|
YAML
|
|
|
|
field = ThemeField.create!(theme_id: -1, target_id: 3, name: "yaml", value: yaml)
|
|
parsed = []
|
|
ThemeSettingsParser
|
|
.new(field)
|
|
.load { |name, default, type, opts| parsed << { name: name, opts: opts } }
|
|
|
|
setting = parsed.find { |s| s[:name] == :groups_setting }
|
|
expect(setting[:opts][:resolve_group_membership]).to eq(true)
|
|
end
|
|
|
|
it "defaults to false when not specified" do
|
|
yaml = <<~YAML
|
|
groups_setting:
|
|
type: list
|
|
list_type: group
|
|
default: "1|2"
|
|
YAML
|
|
|
|
field = ThemeField.create!(theme_id: -1, target_id: 3, name: "yaml", value: yaml)
|
|
parsed = []
|
|
ThemeSettingsParser
|
|
.new(field)
|
|
.load { |name, default, type, opts| parsed << { name: name, opts: opts } }
|
|
|
|
setting = parsed.find { |s| s[:name] == :groups_setting }
|
|
expect(setting[:opts][:resolve_group_membership]).to eq(false)
|
|
end
|
|
end
|
|
end
|