0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-ai/lib/guardian_extensions.rb
Roman Rizzi 0be4c8f73a SECURITY: Force regeneration for edit-outdated summaries and block stale fallback [backport 2026.1]
Backport of #568 to release/2026.1.

---

Cached topic summaries that became outdated after post edits could remain visible longer than intended, and the 1-hour throttle could delay corrective regeneration. This update makes edit-driven staleness a special case: users who can regenerate summaries now bypass the throttle and refresh immediately, while users who cannot regenerate are not served edit-outdated cached content at all. In practice, stale summaries from edited content are no longer a fallback path, but normal throttle behavior is preserved for non-edit staleness such as newly added posts.

---

**Security Advisory:** https://github.com/discourse/discourse/security/advisories/GHSA-pr9m-5hpq-wc57
2026-03-31 15:12:50 +01:00

119 lines
3.6 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module GuardianExtensions
def can_see_summary?(target, cached_summary:)
return false if !SiteSetting.ai_summarization_enabled
if target.class == Topic && target.private_message?
allowed =
SiteSetting.ai_pm_summarization_allowed_groups_map.any? do |group_id|
user.group_ids.include?(group_id)
end
return false if !allowed
end
can_summarize = can_request_summary?
return true if can_summarize
return false if cached_summary.nil?
cached_summary.present? && !cached_summary.outdated
end
def can_see_gists?
return false if !SiteSetting.ai_summarization_enabled
return false if !SiteSetting.ai_summary_gists_enabled
if (ai_persona = AiPersona.find_by_id_from_cache(SiteSetting.ai_summary_gists_persona)).blank?
return false
end
persona_groups = ai_persona.allowed_group_ids.to_a
return true if persona_groups.include?(Group::AUTO_GROUPS[:everyone])
return false if anonymous?
persona_groups.any? { |group_id| user.group_ids.include?(group_id) }
end
def can_request_gists?
return false if !SiteSetting.ai_summarization_enabled
return false if !SiteSetting.ai_summary_gists_enabled
return false if !AiPersona.exists?(id: SiteSetting.ai_summary_gists_persona)
is_staff?
end
def can_regenerate_summary?
return false if !SiteSetting.ai_summarization_enabled
return false if !AiPersona.exists?(id: SiteSetting.ai_summarization_persona)
is_staff?
end
def can_request_summary?
return false if anonymous?
user_group_ids = user.group_ids
if (ai_persona = AiPersona.find_by_id_from_cache(SiteSetting.ai_summarization_persona)).blank?
return false
end
ai_persona.allowed_group_ids.to_a.any? { |group_id| user.group_ids.include?(group_id) }
end
def can_debug_ai_bot_conversation?(target)
return false if anonymous?
return false if !can_see?(target)
if !SiteSetting.discourse_ai_enabled || !SiteSetting.ai_bot_enabled ||
!SiteSetting.ai_bot_debugging_allowed_groups_map.any?
return false
end
user.in_any_groups?(SiteSetting.ai_bot_debugging_allowed_groups_map)
end
def can_share_ai_bot_conversation?(target)
return false if anonymous?
if !SiteSetting.discourse_ai_enabled || !SiteSetting.ai_bot_enabled ||
!SiteSetting.ai_bot_public_sharing_allowed_groups_map.any?
return false
end
return false if !user.in_any_groups?(SiteSetting.ai_bot_public_sharing_allowed_groups_map)
# In future we may add other valid targets for AI conversation sharing,
# for now we only support topics.
if target.is_a?(Topic)
return false if !target.private_message?
return false if target.topic_allowed_groups.exists?
allowed_user_ids = target.topic_allowed_users.pluck(:user_id)
# not in PM
return false if !allowed_user_ids.include?(user.id)
# other people in PM
return false if allowed_user_ids.any? { |id| id > 0 && id != user.id }
# no bot in the PM
bot_ids = DiscourseAi::AiBot::EntryPoint.all_bot_ids
return false if allowed_user_ids.none? { |id| bot_ids.include?(id) }
# other content in PM
return false if target.posts.where("user_id > 0 and user_id <> ?", user.id).exists?
else
return false
end
true
end
def can_destroy_shared_ai_bot_conversation?(conversation)
return false if anonymous?
conversation.user_id == user.id || is_admin?
end
end
end