0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/app/serializers/theme_settings_serializer.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

126 lines
2.5 KiB
Ruby
Vendored

# frozen_string_literal: true
class ThemeSettingsSerializer < ApplicationSerializer
attributes :setting,
:humanized_name,
:type,
:default,
:value,
:description,
:valid_values,
:list_type,
:resolve_group_membership,
:textarea,
:json_schema,
:objects_schema
def setting
object.name
end
def humanized_name
SiteSetting.humanized_name(object.name)
end
def type
object.type_name
end
def default
object.default
end
def value
object.value
end
def description
description_regexp = /^theme_metadata\.settings\.#{setting}(\.description)?$/
locale_file_description =
object.theme.internal_translations.find { |t| t.key.match?(description_regexp) }&.value
resolved_description = locale_file_description || object.description
if resolved_description
catch(:exception) do
return I18n.interpolate(resolved_description, base_path: Discourse.base_path)
end
resolved_description
end
end
def valid_values
choices = object.choices
labels = choice_labels
choices.map do |choice|
label = labels[choice.to_s]
label ? { name: label, value: choice } : choice
end
end
def include_valid_values?
object.type == ThemeSetting.types[:enum]
end
def include_description?
description.present?
end
def list_type
object.list_type
end
def include_list_type?
object.type == ThemeSetting.types[:list]
end
def textarea
object.textarea
end
def include_textarea?
object.type == ThemeSetting.types[:string]
end
def objects_schema
object.schema
end
def include_objects_schema?
object.type == ThemeSetting.types[:objects]
end
def json_schema
object.json_schema
end
def include_json_schema?
object.type == ThemeSetting.types[:string] && object.json_schema.present?
end
def resolve_group_membership
object.resolve_group_membership?
end
def include_resolve_group_membership?
object.type == ThemeSetting.types[:list] && object.list_type == "group"
end
private
def choice_labels
labels = {}
key_prefix = "theme_metadata.settings.#{setting}.choices."
object.theme.internal_translations.each do |translation|
if translation.key.start_with?(key_prefix)
choice_key = translation.key.delete_prefix(key_prefix)
labels[choice_key] = translation.value
end
end
labels
end
end