mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
DEV: Add disallowed_groups to theme/component settings (#41792)
Further following on from bee1be8599,
this brings greater parity between theme settings and site settings.
This commit adds a new `disallowed_groups` field to theme and component
settings, which allows theme authors to specify groups that cannot be
selected
by admins for a particular list type or object group type setting.
This is useful for cases where the group-based setting isn't usable
for a group like e.g. anonymous_users, no point allowing them for
a setting that requires a user to be logged in to take effect.
c.f.
https://meta.discourse.org/t/granular-group-based-permissions-for-anonymous-and-logged-in-users/402273/18?u=martin
### List groups
With `disallowed_groups: "4|5"` specified (`logged_in_users` &
`anonymous_users`)
<img width="734" height="450" alt="image"
src="https://github.com/user-attachments/assets/ed8d7338-65fa-44e6-8dce-7073a87076e6"
/>
### Object groups
With `disallowed_groups: "4|5"` specified (`logged_in_users` &
`anonymous_users`)
<img width="696" height="725" alt="image"
src="https://github.com/user-attachments/assets/1a9103d4-cf44-4038-9d62-3838cb779b49"
/>
This commit is contained in:
@@ -9,6 +9,7 @@ class ThemeSettingsSerializer < ApplicationSerializer
|
||||
:description,
|
||||
:valid_values,
|
||||
:list_type,
|
||||
:disallowed_groups,
|
||||
:resolve_group_membership,
|
||||
:textarea,
|
||||
:json_schema,
|
||||
@@ -76,6 +77,14 @@ class ThemeSettingsSerializer < ApplicationSerializer
|
||||
object.type == ThemeSetting.types[:list]
|
||||
end
|
||||
|
||||
def disallowed_groups
|
||||
object.disallowed_groups
|
||||
end
|
||||
|
||||
def include_disallowed_groups?
|
||||
object.disallowed_groups.present?
|
||||
end
|
||||
|
||||
def textarea
|
||||
object.textarea
|
||||
end
|
||||
|
||||
@@ -9,6 +9,16 @@ export default class SchemaSettingTypeGroups extends SchemaSettingTypeModels {
|
||||
|
||||
type = "groups";
|
||||
|
||||
get groupChoices() {
|
||||
const disallowed = (this.args.spec.disallowed_groups || "")
|
||||
.split("|")
|
||||
.filter(Boolean);
|
||||
|
||||
return (this.site.groups || []).filter(
|
||||
(group) => !disallowed.includes(group.id.toString())
|
||||
);
|
||||
}
|
||||
|
||||
get groupChooserOptions() {
|
||||
return {
|
||||
clearable: !this.required,
|
||||
@@ -19,7 +29,7 @@ export default class SchemaSettingTypeGroups extends SchemaSettingTypeModels {
|
||||
|
||||
<template>
|
||||
<GroupChooser
|
||||
@content={{this.site.groups}}
|
||||
@content={{this.groupChoices}}
|
||||
@value={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
@options={{this.groupChooserOptions}}
|
||||
|
||||
@@ -1212,6 +1212,63 @@ module(
|
||||
.hasText("You can only select 3 items.");
|
||||
});
|
||||
|
||||
test("input fields of type groups filter disallowed groups", async function (assert) {
|
||||
this.site.groups = [
|
||||
{ id: 0, name: "everyone" },
|
||||
{ id: 1, name: "admins" },
|
||||
{ id: 2, name: "moderators" },
|
||||
];
|
||||
|
||||
const setting = ThemeSettings.create({
|
||||
setting: "objects_setting",
|
||||
objects_schema: {
|
||||
name: "something",
|
||||
properties: {
|
||||
group_ids: {
|
||||
type: "groups",
|
||||
disallowed_groups: "0|1",
|
||||
},
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
group_ids: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await render(
|
||||
<template>
|
||||
<AdminSchemaSettingEditor
|
||||
@id="1"
|
||||
@setting={{setting}}
|
||||
@schema={{setting.objects_schema}}
|
||||
@routeToRedirect="adminCustomizeThemes.show"
|
||||
/>
|
||||
</template>
|
||||
);
|
||||
|
||||
const inputFields = new InputFieldsFromDOM();
|
||||
const groupsSelector = selectKit(
|
||||
`${inputFields.fields.group_ids.selector} .select-kit`
|
||||
);
|
||||
|
||||
await groupsSelector.expand();
|
||||
|
||||
assert.false(
|
||||
groupsSelector.rowByValue("0").exists(),
|
||||
"everyone is not in the list"
|
||||
);
|
||||
assert.false(
|
||||
groupsSelector.rowByValue("1").exists(),
|
||||
"admins is not in the list"
|
||||
);
|
||||
assert.true(
|
||||
groupsSelector.rowByValue("2").exists(),
|
||||
"moderators is in the list"
|
||||
);
|
||||
});
|
||||
|
||||
test("generic identifier is used when identifier is not specified in the schema", async function (assert) {
|
||||
const setting = ThemeSettings.create({
|
||||
setting: "objects_setting",
|
||||
|
||||
@@ -55,6 +55,10 @@ class ThemeSettingsManager
|
||||
@opts[:refresh]
|
||||
end
|
||||
|
||||
def disallowed_groups
|
||||
@opts[:disallowed_groups]
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
ensure_is_valid_value!(new_value)
|
||||
value = new_value.to_s
|
||||
|
||||
@@ -8,4 +8,13 @@ class ThemeSettingsManager::List < ThemeSettingsManager
|
||||
def resolve_group_membership?
|
||||
@opts[:resolve_group_membership] && list_type == "group"
|
||||
end
|
||||
|
||||
def value=(new_value)
|
||||
if list_type == "group" && disallowed_groups.present?
|
||||
disallowed_ids = disallowed_groups.to_s.split("|")
|
||||
new_value = new_value.to_s.split("|").reject { |id| disallowed_ids.include?(id) }.join("|")
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@ class ThemeSettingsManager::Objects < ThemeSettingsManager
|
||||
|
||||
def value=(objects)
|
||||
objects = JSON.parse(objects) if objects.is_a?(::String)
|
||||
objects = remove_disallowed_groups(objects)
|
||||
ensure_is_valid_value!(objects)
|
||||
objects = SchemaSettingsObjectValidator.normalize_uploads(schema:, objects:)
|
||||
record = has_record? ? update_record!(json_value: objects) : create_record!(json_value: objects)
|
||||
@@ -30,6 +31,12 @@ class ThemeSettingsManager::Objects < ThemeSettingsManager
|
||||
SchemaSettingsObjectValidator.hydrate_uploads(schema:, objects:, cdn: true)
|
||||
end
|
||||
|
||||
def remove_disallowed_groups(objects)
|
||||
return objects if objects.blank?
|
||||
|
||||
remove_disallowed_groups_from_objects(objects.deep_dup, schema[:properties])
|
||||
end
|
||||
|
||||
def categories(guardian)
|
||||
category_ids = Set.new
|
||||
|
||||
@@ -46,4 +53,41 @@ class ThemeSettingsManager::Objects < ThemeSettingsManager
|
||||
|
||||
Category.secured(guardian).where(id: category_ids)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_disallowed_groups_from_objects(objects, properties)
|
||||
objects.each do |object|
|
||||
properties.each do |property_name, property_attributes|
|
||||
key = object_key(object, property_name)
|
||||
next if key.nil?
|
||||
|
||||
case property_attributes[:type]
|
||||
when "groups"
|
||||
next if property_attributes[:disallowed_groups].blank?
|
||||
|
||||
disallowed_ids = property_attributes[:disallowed_groups].to_s.split("|").map(&:to_i)
|
||||
object[key] = Array(object[key]).reject { |id| disallowed_ids.include?(id) }
|
||||
when "objects"
|
||||
nested_objects = object[key]
|
||||
if nested_objects.is_a?(Array)
|
||||
remove_disallowed_groups_from_objects(
|
||||
nested_objects,
|
||||
property_attributes[:schema][:properties],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
objects
|
||||
end
|
||||
|
||||
def object_key(object, property_name)
|
||||
string_key = property_name.to_s
|
||||
return string_key if object.key?(string_key)
|
||||
|
||||
symbol_key = property_name.to_sym
|
||||
symbol_key if object.key?(symbol_key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,6 +38,7 @@ class ThemeSettingsParser
|
||||
end
|
||||
|
||||
opts[:list_type] = raw_opts[:list_type] if raw_opts[:list_type]
|
||||
opts[:disallowed_groups] = raw_opts[:disallowed_groups] if raw_opts[:disallowed_groups]
|
||||
opts[:resolve_group_membership] = !!raw_opts[:resolve_group_membership]
|
||||
|
||||
opts[:textarea] = !!raw_opts[:textarea]
|
||||
|
||||
@@ -30,6 +30,53 @@ RSpec.describe ThemeSettingsManager::Objects do
|
||||
expect(theme.reload.settings[:objects_setting].value).to eq(new_value)
|
||||
end
|
||||
|
||||
it "removes disallowed group ids before saving groups properties" do
|
||||
theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
|
||||
objects_setting:
|
||||
type: objects
|
||||
default: []
|
||||
schema:
|
||||
name: section
|
||||
properties:
|
||||
group_ids:
|
||||
type: groups
|
||||
disallowed_groups: "#{Group::AUTO_GROUPS[:everyone]}|#{Group::AUTO_GROUPS[:admins]}"
|
||||
links:
|
||||
type: objects
|
||||
schema:
|
||||
name: link
|
||||
properties:
|
||||
group_ids:
|
||||
type: groups
|
||||
disallowed_groups: "#{Group::AUTO_GROUPS[:trust_level_0]}"
|
||||
YAML
|
||||
theme.save!
|
||||
|
||||
theme.settings[:objects_setting].value = [
|
||||
{
|
||||
"group_ids" => [
|
||||
Group::AUTO_GROUPS[:everyone],
|
||||
Group::AUTO_GROUPS[:admins],
|
||||
Group::AUTO_GROUPS[:staff],
|
||||
],
|
||||
"links" => [
|
||||
{
|
||||
"group_ids" => [Group::AUTO_GROUPS[:trust_level_0], Group::AUTO_GROUPS[:trust_level_1]],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
expect(theme.reload.settings[:objects_setting].value).to eq(
|
||||
[
|
||||
{
|
||||
"group_ids" => [Group::AUTO_GROUPS[:staff]],
|
||||
"links" => [{ "group_ids" => [Group::AUTO_GROUPS[:trust_level_1]] }],
|
||||
},
|
||||
],
|
||||
)
|
||||
end
|
||||
|
||||
it "raises the right error when there are objects which are not valid" do
|
||||
new_value = [
|
||||
{ "name" => "section 3", "links" => [{ "url" => "https://some.url.no.name" }] },
|
||||
|
||||
@@ -142,6 +142,23 @@ RSpec.describe ThemeSettingsManager do
|
||||
expect(list_setting.list_type).to eq("compact")
|
||||
end
|
||||
|
||||
it "removes disallowed group ids before saving group list settings" do
|
||||
yaml = <<~YAML
|
||||
groups_setting:
|
||||
type: list
|
||||
list_type: group
|
||||
disallowed_groups: "0|1"
|
||||
default: ""
|
||||
YAML
|
||||
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||
theme.save!
|
||||
|
||||
setting = theme.settings[:groups_setting]
|
||||
setting.value = "0|1|2|3"
|
||||
|
||||
expect(theme.reload.settings[:groups_setting].value).to eq("2|3")
|
||||
end
|
||||
|
||||
describe "#resolve_group_membership?" do
|
||||
it "returns true when opted-in with list_type group" do
|
||||
yaml = <<~YAML
|
||||
|
||||
@@ -87,6 +87,25 @@ RSpec.describe ThemeSettingsParser do
|
||||
list_type = loader.find_by_name(:compact_list_setting)[:opts][:list_type]
|
||||
expect(list_type).to eq("compact")
|
||||
end
|
||||
|
||||
it "supports disallowed groups metadata" do
|
||||
yaml = <<~YAML
|
||||
groups_setting:
|
||||
type: list
|
||||
list_type: group
|
||||
disallowed_groups: "0|1"
|
||||
default: "2|3"
|
||||
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 { |parsed_setting| parsed_setting[:name] == :groups_setting }
|
||||
expect(setting[:opts][:disallowed_groups]).to eq("0|1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "resolve_group_membership" do
|
||||
|
||||
@@ -16,6 +16,44 @@ RSpec.describe ThemeSettingsSerializer do
|
||||
|
||||
expect(payload[:theme_settings][:objects_schema][:name]).to eq("section")
|
||||
end
|
||||
|
||||
it "includes disallowed groups metadata for group properties" do
|
||||
theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
|
||||
objects_setting:
|
||||
type: objects
|
||||
default: []
|
||||
schema:
|
||||
name: section
|
||||
properties:
|
||||
group_ids:
|
||||
type: groups
|
||||
disallowed_groups: "0|1"
|
||||
YAML
|
||||
theme.save!
|
||||
|
||||
payload = ThemeSettingsSerializer.new(theme.reload.settings[:objects_setting]).as_json
|
||||
|
||||
expect(
|
||||
payload[:theme_settings][:objects_schema][:properties][:group_ids][:disallowed_groups],
|
||||
).to eq("0|1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#disallowed_groups" do
|
||||
it "includes disallowed groups metadata for group list settings" do
|
||||
theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
|
||||
groups_setting:
|
||||
type: list
|
||||
list_type: group
|
||||
disallowed_groups: "0|1"
|
||||
default: "2|3"
|
||||
YAML
|
||||
theme.save!
|
||||
|
||||
payload = ThemeSettingsSerializer.new(theme.reload.settings[:groups_setting]).as_json
|
||||
|
||||
expect(payload[:theme_settings][:disallowed_groups]).to eq("0|1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#valid_values" do
|
||||
|
||||
Reference in New Issue
Block a user