0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/discourse-calendar/lib/discourse_post_event/event_finder.rb
Chris Alberti c253fb0197
SECURITY: Remove stale invitee access for users removed from invited groups on private events (#42187)
## Summary

Correctly restrict private calendar event access to current group
members. The patch replaces stale invitee-row authorization with active
invited-group membership checks across event detail serialization,
attendance searches, RSVP mutations, and livestream chat metadata, and
prunes stale invitee records when a user is removed from a group.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1377

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-07-30 15:15:46 -05:00

210 lines
7 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscoursePostEvent
class EventFinder
def self.search(user, params = {})
guardian = Guardian.new(user)
build_base_query(guardian, user, params)
.then { |query| filter_by_post_id(query, params) }
.then { |query| filter_by_topic_id(query, params) }
.then { |query| filter_by_attending_user(query, params, guardian, user) }
.then { |query| filter_by_dates(query, params) }
.then { |query| filter_by_category(query, params) }
.then { |query| apply_ordering(query, params) }
.then { |query| apply_limit(query, params) }
end
private
def self.build_base_query(guardian, user, params)
topics = listable_topics(guardian)
pms = private_messages(user)
scope = DiscoursePostEvent::Event.visible
scope = scope.open if params[:include_closed].blank?
scope
.joins(post: :topic)
.merge(Post.secured(guardian))
.merge(topics.or(pms))
.joins(latest_event_date_join)
.select(
"discourse_post_event_events.*, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
)
.where(
"(discourse_post_event_events.recurrence IS NOT NULL) OR (latest_event_dates.starts_at IS NOT NULL) OR (discourse_post_event_events.original_starts_at IS NOT NULL)",
)
.group(
"discourse_post_event_events.id, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
)
end
def self.latest_event_date_join
<<~SQL
LEFT JOIN (
SELECT DISTINCT ON (event_id)
event_id,
starts_at,
ends_at,
finished_at
FROM discourse_calendar_post_event_dates
ORDER BY event_id, #{DiscoursePostEvent::EventDate.current_ordering_sql}
) latest_event_dates ON latest_event_dates.event_id = discourse_post_event_events.id
SQL
end
def self.filter_by_post_id(events, params)
return events if params[:post_id].blank?
events.where(id: params[:post_id])
end
def self.filter_by_topic_id(events, params)
return events if params[:topic_id].blank?
events.where(topics: { id: params[:topic_id] })
end
def self.filter_by_attending_user(events, params, guardian, user)
return events if params[:attending_user].blank?
attending_user = User.find_by(username_lower: params[:attending_user].downcase)
return events.none if !attending_user
statuses = [DiscoursePostEvent::Invitee.statuses[:going]]
if params[:include_interested].present? &&
can_include_interested?(guardian, user, attending_user)
statuses << DiscoursePostEvent::Invitee.statuses[:interested]
end
events =
events.joins(:invitees).where(
discourse_post_event_invitees: {
user_id: attending_user.id,
status: statuses,
},
)
guardian.is_admin? ? events : apply_privacy_restrictions(events, user)
end
def self.can_include_interested?(guardian, user, attending_user)
guardian.is_admin? || user&.id == attending_user.id
end
def self.apply_privacy_restrictions(events, user)
private_status = DiscoursePostEvent::Event.statuses[:private]
# If no user, can only see non-private events
return events.where.not(status: private_status) if user.nil?
# User can see private events if they still belong to an invited group.
events.where(<<~SQL, private_status, private_status, user.id)
discourse_post_event_events.status != ?
OR (
discourse_post_event_events.status = ?
AND EXISTS (
SELECT 1
FROM group_users
INNER JOIN groups ON groups.id = group_users.group_id
WHERE group_users.user_id = ?
AND groups.name = ANY(discourse_post_event_events.raw_invitees)
)
)
SQL
end
def self.filter_by_dates(events, params)
return events if params[:before].blank? && params[:after].blank?
before_date = params[:before] == "now" ? Time.current : params[:before]&.to_datetime
after_date = params[:after] == "now" ? Time.current : params[:after]&.to_datetime
include_ongoing = params[:include_ongoing].present?
recurring_scope = build_recurring_date_scope(after_date, before_date)
non_recurring_scope =
build_non_recurring_date_scope(after_date, before_date, include_ongoing:)
# Apply the combined scope using OR logic
events.merge(recurring_scope.or(non_recurring_scope))
end
private
def self.build_recurring_date_scope(after_date, before_date)
scope = DiscoursePostEvent::Event.where.not(recurrence: nil)
if after_date
# For recurring events: original start date OR recurrence_until should be >= after_date
scope =
scope.where(
"original_starts_at >= ? OR recurrence_until IS NULL OR recurrence_until >= ?",
after_date,
after_date,
)
end
if before_date
# For recurring events: original start date should be < before_date
scope = scope.where("original_starts_at < ?", before_date)
end
scope
end
def self.build_non_recurring_date_scope(after_date, before_date, include_ongoing: false)
scope = DiscoursePostEvent::Event.where(recurrence: nil)
if after_date
if include_ongoing
scope =
scope.where(
"latest_event_dates.starts_at >= ? OR (latest_event_dates.ends_at IS NOT NULL AND latest_event_dates.ends_at >= ?)",
after_date,
after_date,
)
else
scope = scope.where("latest_event_dates.starts_at >= ?", after_date)
end
end
scope = scope.where("latest_event_dates.starts_at < ?", before_date) if before_date
scope
end
def self.filter_by_category(events, params)
return events if params[:category_id].blank?
category_id = params[:category_id].to_i
category_ids =
(
if params[:include_subcategories].present?
Category.subcategory_ids(category_id)
else
[category_id]
end
)
events.where(topics: { category_id: category_ids })
end
def self.apply_ordering(events, params)
order_direction = params[:order] == "desc" ? "DESC" : "ASC"
events.order(
"latest_event_dates.starts_at #{order_direction}, discourse_post_event_events.id #{order_direction}",
)
end
def self.apply_limit(events, params)
limit = params[:limit]&.to_i || 200
events.limit(limit.clamp(1, 200))
end
def self.listable_topics(guardian)
topics = Topic.listable_topics.secured(guardian)
topics = topics.visible unless guardian.can_see_unlisted_topics?
topics
end
def self.private_messages(user)
user ? Topic.private_messages_for_user(user) : Topic.none
end
end
end