mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 12:53:30 +08:00
Introduces a server-side definition of banned_acl,
similar to mandatory_acl from 5823e4e3b2
This allows AclTarget implementing classes to define
which ACLs cannot be used for certain types. For example:
```
def self.banned_acl
[{ type: :group, id: Group::AUTO_GROUPS[:anonymous_users], permission: "edit" }]
end
```
This prevents anonymous users group from being able to have the Edit
permission on the target, which is practical because the anonymous
user is not logged in and generally cannot create/edit anything.
This restriction is passed to the DAccessControl component via the Site
serializer, same as mandatory_acl, and is used to prevent the user from
selecting the banned ACLs in the UI.
Then, server-side this rule is enforced in `AccessControlListManager`
service as a policy.
**Before**
<img width="971" height="287" alt="image"
src="https://github.com/user-attachments/assets/af4ace0f-7b11-4005-8344-774d554693f0"
/>
**After**
<img width="914" height="194" alt="image"
src="https://github.com/user-attachments/assets/b756b976-f6e6-4331-b27d-c9a2d13997aa"
/>
324 lines
9.8 KiB
Ruby
Vendored
324 lines
9.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
# A class we can use to serialize the site data
|
|
class Site
|
|
include ActiveModel::Serialization
|
|
|
|
cattr_accessor :preloaded_category_custom_fields
|
|
|
|
def self.reset_preloaded_category_custom_fields
|
|
self.preloaded_category_custom_fields = Set.new
|
|
end
|
|
reset_preloaded_category_custom_fields
|
|
|
|
##
|
|
# Sometimes plugins need to have additional data or options available
|
|
# when rendering custom markdown features/rules that are not available
|
|
# on the default opts.discourse object. These additional options should
|
|
# be namespaced to the plugin adding them.
|
|
#
|
|
# ```
|
|
# Site.markdown_additional_options["chat"] = { limited_pretty_text_markdown_rules: [] }
|
|
# ```
|
|
#
|
|
# These are passed down to markdown rules on opts.discourse.additionalOptions.
|
|
cattr_accessor :markdown_additional_options
|
|
self.markdown_additional_options = {}
|
|
|
|
def self.add_categories_callbacks(enabled: -> { true }, &block)
|
|
categories_callbacks << { block:, enabled: }
|
|
end
|
|
|
|
def self.categories_callbacks
|
|
@categories_callbacks ||= []
|
|
end
|
|
|
|
def initialize(guardian)
|
|
@guardian = guardian
|
|
end
|
|
|
|
def site_setting
|
|
SiteSetting
|
|
end
|
|
|
|
def notification_types
|
|
Notification.types
|
|
end
|
|
|
|
def trust_levels
|
|
TrustLevel.levels
|
|
end
|
|
|
|
def user_fields
|
|
UserField.includes(:user_field_options).order(:position).all
|
|
end
|
|
|
|
def access_control
|
|
self.class.access_control
|
|
end
|
|
|
|
def self.access_control
|
|
target_classes =
|
|
(
|
|
AclTarget.target_classes +
|
|
DiscoursePluginRegistry.acl_target_classes.filter_map do |target_class|
|
|
if target_class.is_a?(String)
|
|
target_class = target_class.safe_constantize
|
|
if target_class.nil?
|
|
Rails.logger.warn(
|
|
"[ACL] Unknown target class in plugin registry for site (#{target_class}) maybe the plugin is gone, the class has been renamed, or the class does not include AclTarget",
|
|
)
|
|
end
|
|
target_class
|
|
else
|
|
target_class
|
|
end
|
|
end
|
|
).compact.uniq
|
|
|
|
{
|
|
mandatory_acl:
|
|
target_classes.each_with_object({}) do |target_class, mandatory_acl|
|
|
next if !target_class.respond_to?(:has_mandatory_acl?)
|
|
next if !target_class.has_mandatory_acl?
|
|
|
|
mandatory_acl[target_class.acl_target_key] = target_class.mandatory_acl
|
|
end,
|
|
banned_acl:
|
|
target_classes.each_with_object({}) do |target_class, banned_acl|
|
|
next if !target_class.respond_to?(:has_banned_acl?)
|
|
next if !target_class.has_banned_acl?
|
|
|
|
banned_acl[target_class.acl_target_key] = target_class.banned_acl
|
|
end,
|
|
}
|
|
end
|
|
|
|
def self.categories_cache_key
|
|
"site_categories_#{I18n.locale}_#{Discourse.git_version}"
|
|
end
|
|
|
|
def self.clear_cache
|
|
Discourse.cache.delete(categories_cache_key)
|
|
end
|
|
|
|
def self.all_categories_cache
|
|
# Categories do not change often so there is no need for us to run the
|
|
# same query and spend time creating ActiveRecord objects for every requests.
|
|
#
|
|
# Do note that any new association added to the eager loading needs a
|
|
# corresponding ActiveRecord callback to clear the categories cache.
|
|
Discourse
|
|
.cache
|
|
.fetch(categories_cache_key, expires_in: 30.minutes) do
|
|
categories =
|
|
begin
|
|
query =
|
|
Category
|
|
.includes(
|
|
:uploaded_logo,
|
|
:uploaded_logo_dark,
|
|
:uploaded_background,
|
|
:uploaded_background_dark,
|
|
:tags,
|
|
:tag_groups,
|
|
:form_templates,
|
|
category_required_tag_groups: :tag_group,
|
|
)
|
|
.joins("LEFT JOIN topics t on t.id = categories.topic_id")
|
|
.select("categories.*, t.slug topic_slug")
|
|
.order(:position)
|
|
query =
|
|
DiscoursePluginRegistry.apply_modifier(:site_all_categories_cache_query, query, self)
|
|
query.to_a
|
|
end
|
|
|
|
if preloaded_category_custom_fields.present?
|
|
Category.preload_custom_fields(categories, preloaded_category_custom_fields)
|
|
end
|
|
|
|
ActiveModel::ArraySerializer.new(
|
|
categories,
|
|
each_serializer: SiteCategorySerializer,
|
|
).as_json
|
|
end
|
|
end
|
|
|
|
def categories
|
|
if @guardian.can_lazy_load_categories?
|
|
preloaded_category_ids = []
|
|
if @guardian.authenticated?
|
|
sidebar_category_ids = @guardian.user.secured_sidebar_category_ids(@guardian)
|
|
preloaded_category_ids.concat(
|
|
Category.secured(@guardian).ancestors_of(sidebar_category_ids).pluck(:id),
|
|
)
|
|
preloaded_category_ids.concat(sidebar_category_ids)
|
|
end
|
|
end
|
|
|
|
@categories ||=
|
|
begin
|
|
categories = []
|
|
|
|
self.class.all_categories_cache.each do |category|
|
|
if (
|
|
!@guardian.can_lazy_load_categories? ||
|
|
preloaded_category_ids.include?(category[:id])
|
|
) &&
|
|
@guardian.can_see_serialized_category?(
|
|
category_id: category[:id],
|
|
read_restricted: category[:read_restricted],
|
|
)
|
|
categories << category
|
|
end
|
|
end
|
|
|
|
with_children = Set.new
|
|
categories.each { |c| with_children << c[:parent_category_id] if c[:parent_category_id] }
|
|
|
|
allowed_topic_create = nil
|
|
unless @guardian.is_admin?
|
|
allowed_topic_create_ids =
|
|
@guardian.anonymous? ? [] : Category.topic_create_allowed(@guardian).pluck(:id)
|
|
allowed_topic_create = Set.new(allowed_topic_create_ids)
|
|
end
|
|
|
|
by_id = {}
|
|
|
|
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
|
|
default_notification_level = CategoryUser.default_notification_level
|
|
|
|
categories.each do |category|
|
|
category[:notification_level] = notification_levels[category[:id]] ||
|
|
default_notification_level
|
|
category[:permission] = CategoryGroup.permission_types[
|
|
:full
|
|
] if allowed_topic_create&.include?(category[:id]) || @guardian.is_admin?
|
|
category[:has_children] = with_children.include?(category[:id])
|
|
|
|
category[:can_edit] = @guardian.can_edit_serialized_category?(
|
|
category_id: category[:id],
|
|
read_restricted: category[:read_restricted],
|
|
)
|
|
|
|
by_id[category[:id]] = category
|
|
end
|
|
|
|
categories.reject! { |c| c[:parent_category_id] && !by_id[c[:parent_category_id]] }
|
|
|
|
self.class.categories_callbacks.each do |callback|
|
|
next unless callback[:enabled].call
|
|
callback[:block].call(categories, @guardian)
|
|
end
|
|
|
|
categories
|
|
end
|
|
end
|
|
|
|
def groups
|
|
query =
|
|
Group.visible_groups(
|
|
@guardian.user,
|
|
"groups.name ASC",
|
|
include_everyone: !SiteSetting.granular_anonymous_and_logged_in_groups_permissions,
|
|
include_pseudogroups: SiteSetting.granular_anonymous_and_logged_in_groups_permissions,
|
|
).includes(:flair_upload)
|
|
query = DiscoursePluginRegistry.apply_modifier(:site_groups_query, query, self)
|
|
|
|
query
|
|
end
|
|
|
|
def anonymous_sidebar_sections
|
|
SidebarSection
|
|
.public_sections
|
|
.includes(:sidebar_urls)
|
|
.order("(section_type IS NOT NULL) DESC, (public IS TRUE) DESC")
|
|
end
|
|
|
|
def archetypes
|
|
Archetype.list.reject { |t| t.id == Archetype.private_message }
|
|
end
|
|
|
|
def auth_providers
|
|
Discourse.enabled_auth_providers
|
|
end
|
|
|
|
def self.json_for(guardian)
|
|
if guardian.anonymous? && SiteSetting.login_required
|
|
return(
|
|
{
|
|
periods: TopTopic.periods.map(&:to_s),
|
|
filters: Discourse.filters.map(&:to_s),
|
|
user_fields:
|
|
UserField
|
|
.includes(:user_field_options)
|
|
.order(:position)
|
|
.all
|
|
.map { |userfield| UserFieldSerializer.new(userfield, root: false, scope: guardian) },
|
|
auth_providers:
|
|
Discourse.enabled_auth_providers.map do |provider|
|
|
AuthProviderSerializer.new(provider, root: false, scope: guardian)
|
|
end,
|
|
full_name_required_for_signup:,
|
|
full_name_visible_in_signup:,
|
|
tos_url: Discourse.tos_url,
|
|
privacy_policy_url: Discourse.privacy_policy_url,
|
|
upcoming_changes_with_css: UpcomingChanges.including_css,
|
|
}.to_json
|
|
)
|
|
end
|
|
|
|
seq = nil
|
|
use_localized_anon_cache = SiteSetting.content_localization_enabled && guardian.anonymous?
|
|
|
|
locale = I18n.locale
|
|
cache_key = "site_json"
|
|
seq_key = "site_json_seq"
|
|
version_key = "site_json_version"
|
|
|
|
if use_localized_anon_cache
|
|
cache_key += "_#{locale}"
|
|
seq_key += "_#{locale}"
|
|
version_key += "_#{locale}"
|
|
end
|
|
|
|
if guardian.anonymous?
|
|
seq = MessageBus.last_id("/site_json")
|
|
cached_json, cached_seq, cached_version =
|
|
Discourse.redis.mget(cache_key, seq_key, version_key)
|
|
|
|
if cached_json && seq == cached_seq.to_i && Discourse.git_version == cached_version
|
|
return cached_json
|
|
end
|
|
end
|
|
|
|
site = Site.new(guardian)
|
|
json = MultiJson.dump(SiteSerializer.new(site, root: false, scope: guardian))
|
|
|
|
if guardian.anonymous?
|
|
Discourse.redis.multi do |transaction|
|
|
transaction.setex cache_key, 1800, json
|
|
transaction.set seq_key, seq
|
|
transaction.set version_key, Discourse.git_version
|
|
end
|
|
end
|
|
|
|
json
|
|
end
|
|
|
|
SITE_JSON_CHANNEL = "/site_json"
|
|
|
|
def self.clear_anon_cache!
|
|
# publishing forces the sequence up
|
|
# the cache is validated based on the sequence
|
|
MessageBus.publish(SITE_JSON_CHANNEL, "")
|
|
end
|
|
|
|
def self.full_name_required_for_signup
|
|
SiteSetting.full_name_requirement == "required_at_signup"
|
|
end
|
|
|
|
def self.full_name_visible_in_signup
|
|
SiteSetting.full_name_requirement != "hidden_at_signup"
|
|
end
|
|
end
|