mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 16:45:19 +08:00
The current impersonation feature works by signing you in as the user you are impersonating. This has the side effect of invalidating your own session and forcing you to log out and in again. In this experimental implementation you keep your existing session, but DefaultCurrentUserProvider returns the user being impersonated, allowing you to see the site from their perspective.
397 lines
12 KiB
Ruby
397 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# UserHistory stores information about actions that users have taken,
|
|
# like deleting users, changing site settings, dismissing notifications, etc.
|
|
# Use other classes, like StaffActionLogger, to log records to this table.
|
|
class UserHistory < ActiveRecord::Base
|
|
belongs_to :acting_user, class_name: "User"
|
|
belongs_to :target_user, class_name: "User"
|
|
|
|
belongs_to :post
|
|
belongs_to :topic
|
|
belongs_to :category
|
|
|
|
# Each value in the context should be shorter than this
|
|
MAX_CONTEXT_LENGTH = 50_000
|
|
|
|
# We often store multiple values in details, particularly during post edits
|
|
# Let's allow space for 2 values + a little extra for padding
|
|
MAX_DETAILS_LENGTH = 110_000
|
|
|
|
MAX_JSON_LENGTH = 300_000
|
|
|
|
validates :details, length: { maximum: MAX_DETAILS_LENGTH }
|
|
validates :context, length: { maximum: MAX_CONTEXT_LENGTH }
|
|
validates :subject, length: { maximum: MAX_CONTEXT_LENGTH }
|
|
validates :ip_address, length: { maximum: MAX_CONTEXT_LENGTH }
|
|
validates :email, length: { maximum: MAX_CONTEXT_LENGTH }
|
|
validates :previous_value, length: { maximum: MAX_JSON_LENGTH }
|
|
validates :new_value, length: { maximum: MAX_JSON_LENGTH }
|
|
|
|
validates_presence_of :action
|
|
|
|
scope :only_staff_actions, -> { where("action IN (?)", UserHistory.staff_action_ids) }
|
|
|
|
before_save :set_admin_only
|
|
|
|
def self.actions
|
|
@actions ||=
|
|
Enum.new(
|
|
delete_user: 1,
|
|
change_trust_level: 2,
|
|
change_site_setting: 3,
|
|
change_theme: 4,
|
|
delete_theme: 5,
|
|
checked_for_custom_avatar: 6, # not used anymore
|
|
notified_about_avatar: 7,
|
|
notified_about_sequential_replies: 8,
|
|
notified_about_dominating_topic: 9,
|
|
suspend_user: 10,
|
|
unsuspend_user: 11,
|
|
facebook_no_email: 12, # not used anymore
|
|
grant_badge: 13,
|
|
revoke_badge: 14,
|
|
auto_trust_level_change: 15,
|
|
check_email: 16,
|
|
delete_post: 17,
|
|
delete_topic: 18,
|
|
impersonate: 19,
|
|
roll_up: 20,
|
|
change_username: 21,
|
|
custom: 22,
|
|
custom_staff: 23,
|
|
anonymize_user: 24,
|
|
reviewed_post: 25,
|
|
change_category_settings: 26,
|
|
delete_category: 27,
|
|
create_category: 28,
|
|
change_site_text: 29,
|
|
silence_user: 30,
|
|
unsilence_user: 31,
|
|
grant_admin: 32,
|
|
revoke_admin: 33,
|
|
grant_moderation: 34,
|
|
revoke_moderation: 35,
|
|
backup_create: 36,
|
|
rate_limited_like: 37, # not used anymore
|
|
revoke_email: 38,
|
|
deactivate_user: 39,
|
|
wizard_step: 40,
|
|
lock_trust_level: 41,
|
|
unlock_trust_level: 42,
|
|
activate_user: 43,
|
|
change_readonly_mode: 44,
|
|
backup_download: 45,
|
|
backup_destroy: 46,
|
|
notified_about_get_a_room: 47,
|
|
change_name: 48,
|
|
post_locked: 49,
|
|
post_unlocked: 50,
|
|
check_personal_message: 51,
|
|
disabled_second_factor: 52,
|
|
post_edit: 53,
|
|
topic_published: 54,
|
|
recover_topic: 55,
|
|
post_approved: 56,
|
|
create_badge: 57,
|
|
change_badge: 58,
|
|
delete_badge: 59,
|
|
removed_silence_user: 60,
|
|
removed_suspend_user: 61,
|
|
removed_unsilence_user: 62,
|
|
removed_unsuspend_user: 63,
|
|
post_rejected: 64,
|
|
merge_user: 65,
|
|
entity_export: 66,
|
|
change_password: 67,
|
|
topic_timestamps_changed: 68,
|
|
approve_user: 69,
|
|
web_hook_create: 70,
|
|
web_hook_update: 71,
|
|
web_hook_destroy: 72,
|
|
embeddable_host_create: 73,
|
|
embeddable_host_update: 74,
|
|
embeddable_host_destroy: 75,
|
|
web_hook_deactivate: 76,
|
|
change_theme_setting: 77,
|
|
disable_theme_component: 78,
|
|
enable_theme_component: 79,
|
|
api_key_create: 80,
|
|
api_key_update: 81,
|
|
api_key_destroy: 82,
|
|
revoke_title: 83,
|
|
change_title: 84,
|
|
override_upload_secure_status: 85,
|
|
page_published: 86,
|
|
page_unpublished: 87,
|
|
add_email: 88,
|
|
update_email: 89,
|
|
destroy_email: 90,
|
|
topic_closed: 91,
|
|
topic_opened: 92,
|
|
topic_archived: 93,
|
|
topic_unarchived: 94,
|
|
post_staff_note_create: 95,
|
|
post_staff_note_destroy: 96,
|
|
watched_word_create: 97,
|
|
watched_word_destroy: 98,
|
|
delete_group: 99,
|
|
permanently_delete_post_revisions: 100,
|
|
create_public_sidebar_section: 101,
|
|
update_public_sidebar_section: 102,
|
|
destroy_public_sidebar_section: 103,
|
|
reset_bounce_score: 104,
|
|
create_watched_word_group: 105,
|
|
update_watched_word_group: 106,
|
|
delete_watched_word_group: 107,
|
|
redirected_to_required_fields: 108,
|
|
filled_in_required_fields: 109,
|
|
topic_slow_mode_set: 110,
|
|
topic_slow_mode_removed: 111,
|
|
custom_emoji_create: 112,
|
|
custom_emoji_destroy: 113,
|
|
delete_post_permanently: 114,
|
|
delete_topic_permanently: 115,
|
|
tag_group_create: 116,
|
|
tag_group_destroy: 117,
|
|
tag_group_change: 118,
|
|
delete_associated_accounts: 119,
|
|
change_theme_site_setting: 120,
|
|
stop_impersonating: 121,
|
|
)
|
|
end
|
|
|
|
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
|
|
def self.staff_actions
|
|
@staff_actions ||= %i[
|
|
delete_user
|
|
change_trust_level
|
|
change_site_setting
|
|
change_theme
|
|
delete_theme
|
|
change_site_text
|
|
suspend_user
|
|
unsuspend_user
|
|
removed_suspend_user
|
|
removed_unsuspend_user
|
|
grant_badge
|
|
revoke_badge
|
|
check_email
|
|
delete_post
|
|
delete_topic
|
|
impersonate
|
|
roll_up
|
|
change_username
|
|
custom_staff
|
|
anonymize_user
|
|
reviewed_post
|
|
change_category_settings
|
|
delete_category
|
|
create_category
|
|
silence_user
|
|
unsilence_user
|
|
removed_silence_user
|
|
removed_unsilence_user
|
|
grant_admin
|
|
revoke_admin
|
|
grant_moderation
|
|
revoke_moderation
|
|
backup_create
|
|
revoke_email
|
|
deactivate_user
|
|
lock_trust_level
|
|
unlock_trust_level
|
|
activate_user
|
|
change_readonly_mode
|
|
backup_download
|
|
backup_destroy
|
|
post_locked
|
|
post_unlocked
|
|
check_personal_message
|
|
disabled_second_factor
|
|
post_edit
|
|
topic_published
|
|
recover_topic
|
|
post_approved
|
|
create_badge
|
|
change_badge
|
|
delete_badge
|
|
post_rejected
|
|
merge_user
|
|
entity_export
|
|
change_name
|
|
topic_timestamps_changed
|
|
approve_user
|
|
web_hook_create
|
|
web_hook_update
|
|
web_hook_destroy
|
|
web_hook_deactivate
|
|
embeddable_host_create
|
|
embeddable_host_update
|
|
embeddable_host_destroy
|
|
change_theme_setting
|
|
disable_theme_component
|
|
enable_theme_component
|
|
revoke_title
|
|
change_title
|
|
api_key_create
|
|
api_key_update
|
|
api_key_destroy
|
|
override_upload_secure_status
|
|
page_published
|
|
page_unpublished
|
|
add_email
|
|
update_email
|
|
destroy_email
|
|
topic_closed
|
|
topic_opened
|
|
topic_archived
|
|
topic_unarchived
|
|
post_staff_note_create
|
|
post_staff_note_destroy
|
|
watched_word_create
|
|
watched_word_destroy
|
|
delete_group
|
|
permanently_delete_post_revisions
|
|
create_public_sidebar_section
|
|
update_public_sidebar_section
|
|
destroy_public_sidebar_section
|
|
reset_bounce_score
|
|
update_directory_columns
|
|
deleted_unused_tags
|
|
renamed_tag
|
|
deleted_tag
|
|
chat_channel_status_change
|
|
chat_auto_remove_membership
|
|
create_watched_word_group
|
|
update_watched_word_group
|
|
delete_watched_word_group
|
|
topic_slow_mode_set
|
|
topic_slow_mode_removed
|
|
custom_emoji_create
|
|
custom_emoji_destroy
|
|
delete_post_permanently
|
|
delete_topic_permanently
|
|
tag_group_create
|
|
tag_group_destroy
|
|
tag_group_change
|
|
delete_associated_accounts
|
|
toggle_flag
|
|
delete_flag
|
|
update_flag
|
|
create_flag
|
|
change_theme_site_setting
|
|
stop_impersonating
|
|
]
|
|
end
|
|
|
|
def self.staff_action_ids
|
|
@staff_action_ids ||= staff_actions.map { |a| actions[a] }
|
|
end
|
|
|
|
def self.admin_only_action_ids
|
|
@admin_only_action_ids ||= [actions[:change_site_setting]]
|
|
end
|
|
|
|
def self.with_filters(filters)
|
|
query = self
|
|
query = query.where(action: filters[:action_id]) if filters[:action_id].present?
|
|
query = query.where(custom_type: filters[:custom_type]) if filters[:custom_type].present?
|
|
|
|
%i[acting_user target_user].each do |key|
|
|
if filters[key] && (obj_id = User.where(username_lower: filters[key].downcase).pluck(:id))
|
|
query = query.where("#{key}_id = ?", obj_id)
|
|
end
|
|
end
|
|
query = query.where("subject = ?", filters[:subject]) if filters[:subject]
|
|
query
|
|
end
|
|
|
|
def self.for(user, action_type)
|
|
self.where(target_user_id: user.id, action: UserHistory.actions[action_type])
|
|
end
|
|
|
|
def self.exists_for_user?(user, action_type, opts = nil)
|
|
opts = opts || {}
|
|
result = self.where(target_user_id: user.id, action: UserHistory.actions[action_type])
|
|
result = result.where(topic_id: opts[:topic_id]) if opts[:topic_id]
|
|
result.exists?
|
|
end
|
|
|
|
def self.staff_filters
|
|
%i[action_id custom_type acting_user target_user subject action_name]
|
|
end
|
|
|
|
def self.staff_action_records(viewer, opts = nil)
|
|
opts ||= {}
|
|
custom_staff = opts[:action_id].to_i == actions[:custom_staff]
|
|
|
|
if custom_staff
|
|
opts[:custom_type] = opts[:action_name]
|
|
else
|
|
opts[:action_id] = self.actions[opts[:action_name].to_sym] if opts[:action_name]
|
|
end
|
|
|
|
query =
|
|
self
|
|
.with_filters(opts.slice(*staff_filters))
|
|
.only_staff_actions
|
|
.order("id DESC")
|
|
.includes(:acting_user, :target_user)
|
|
query = query.where(admin_only: false) unless viewer && viewer.admin?
|
|
|
|
query = query.where("created_at >= ?", opts[:start_date].to_time) if opts[:start_date]
|
|
query = query.where("created_at <= ?", opts[:end_date].to_time) if opts[:end_date]
|
|
|
|
query
|
|
end
|
|
|
|
def set_admin_only
|
|
self.admin_only = UserHistory.admin_only_action_ids.include?(self.action)
|
|
self
|
|
end
|
|
|
|
def new_value_is_json?
|
|
%i[change_theme delete_theme tag_group_create tag_group_destroy tag_group_change]
|
|
.map { |i| UserHistory.actions[i] }
|
|
.include?(action)
|
|
end
|
|
|
|
def previous_value_is_json?
|
|
new_value_is_json?
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: user_histories
|
|
#
|
|
# id :integer not null, primary key
|
|
# action :integer not null
|
|
# acting_user_id :integer
|
|
# target_user_id :integer
|
|
# details :text
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# context :string
|
|
# ip_address :string
|
|
# email :string
|
|
# subject :text
|
|
# previous_value :text
|
|
# new_value :text
|
|
# topic_id :integer
|
|
# admin_only :boolean default(FALSE)
|
|
# post_id :integer
|
|
# custom_type :string
|
|
# category_id :integer
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_user_histories_on_acting_user_id_and_action_and_id (acting_user_id,action,id)
|
|
# index_user_histories_on_action_and_id (action,id)
|
|
# index_user_histories_on_category_id (category_id)
|
|
# index_user_histories_on_post_id (post_id)
|
|
# index_user_histories_on_subject_and_id (subject,id)
|
|
# index_user_histories_on_target_user_id_and_id (target_user_id,id)
|
|
# index_user_histories_on_topic_id_and_target_user_id_and_action (topic_id,target_user_id,action)
|
|
#
|