0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-calendar/jobs/regular/discourse_post_event/bulk_invite.rb
Régis Hanol baa573d414
FIX: Enforce group visibility when filtering users by group (#41370)
Previously, filtering a user search by group (e.g.
`/u/search/users?groups=admins`) only checked whether the requester
could see the group's members, not the group itself. Because the default
automatic groups are visible to logged-in users but expose their members
publicly, an anonymous or non-member user could enumerate the members of
a group hidden from them, even though every other path that lists users
by group (the user directory, `/g/:name/members`, group topic lists)
already required both checks.

This adds a `Guardian#can_see_group_and_members?` helper that requires
both group and member visibility, and routes the user search, user
directory, and plugin group-listing paths through it. This closes the
leak, fixes a related all-or-nothing bug in `can_see_groups?` when
several groups are filtered at once, and keeps the rule in one place so
a caller can no longer reintroduce it by checking only half.
2026-07-03 07:08:52 +02:00

147 lines
4.1 KiB
Ruby
Vendored

# frozen_string_literal: true
module Jobs
class DiscoursePostEventBulkInvite < ::Jobs::Base
sidekiq_options retry: false
def initialize
super
@logs = []
@processed = 0
@failed = 0
end
def execute(args)
invitees = args[:invitees]
raise Discourse::InvalidParameters.new(:invitees) if invitees.blank?
@current_user = User.find_by(id: args[:current_user_id])
raise Discourse::InvalidParameters.new(:current_user_id) unless @current_user
@event = DiscoursePostEvent::Event.find_by(id: args[:event_id])
raise Discourse::InvalidParameters.new(:event_id) unless @event
@guardian = Guardian.new(@current_user)
@guardian.ensure_can_edit!(@event.post)
process_invitees(invitees)
ensure
notify_user
end
private
def process_invitees(invitees)
invitees = invitees.map(&:with_indifferent_access)
invitees = filter_out_unavailable_groups(invitees)
max_bulk_invitees = SiteSetting.discourse_post_event_max_bulk_invitees
invitees.each do |invitee|
break if @processed >= max_bulk_invitees
process_invitee(invitee)
end
if @processed > 0
@event.publish_update!
@event.notify_invitees!(predefined_attendance: true)
end
rescue Exception => e
save_log "Bulk Invite Process Failed -- '#{e.message}'"
@failed += 1
end
def process_invitee(invitee)
if @event.public?
users = User.where(username_lower: invitee["identifier"].downcase).pluck(:id)
else
group = Group.find_by(name: invitee["identifier"])
if group
users = group.users.pluck(:id)
@event.update_with_params!(
raw_invitees: (@event.raw_invitees || []).push(group.name).uniq,
)
end
end
if users.blank?
save_log "Couldn't find user or group: '#{invitee["identifier"]}' or the groups provided contained no users. Note that public events can't bulk invite groups. And other events can't bulk invite usernames."
@failed += 1
return
end
attendance = invitee["attendance"] || "going"
status = DiscoursePostEvent::Invitee.statuses[attendance.to_sym]
if status.nil?
save_log "Skipping '#{invitee["identifier"]}' due to unknown attendance: '#{attendance}'"
@failed += 1
return
end
post_id = @event.post.id
users.each do |user_id|
# Respect capacity: skip creating new going when full
if attendance == "going" && @event.at_capacity?
save_log "Skipping '#{invitee["identifier"]}' due to max attendees reached"
@failed += 1
next
end
create_attendance(user_id, post_id, status)
end
@processed += 1
rescue Exception => e
save_log "Bulk Invite Process Failed -- '#{e.message}'"
@failed += 1
end
def create_attendance(user_id, post_id, status)
invitee =
DiscoursePostEvent::Invitee.find_or_initialize_by(user_id: user_id, post_id: post_id)
invitee.notified = false
invitee.status = status
invitee.save!
end
def save_log(message)
@logs << "[#{Time.zone.now}] #{message}"
end
def notify_user
if @current_user
if @processed > 0 && @failed == 0
SystemMessage.create_from_system_user(
@current_user,
:discourse_post_event_bulk_invite_succeeded,
processed: @processed,
)
else
SystemMessage.create_from_system_user(
@current_user,
:discourse_post_event_bulk_invite_failed,
processed: @processed,
failed: @failed,
logs: @logs.join("\n"),
)
end
end
end
def invitee_groups(invitees)
Group.where(name: invitees.map { |i| i[:identifier] })
end
def filter_out_unavailable_groups(invitees)
groups = invitee_groups(invitees)
invitees.filter do |i|
group = groups.find { |g| g.name === i[:identifier] }
!group || @guardian.can_see_group_and_members?(group)
end
end
end
end