1
0
Fork 0
forked from fedi/mastodon

Add logging of admin actions (#5757)

* Add logging of admin actions

* Update brakeman whitelist

* Log creates, updates and destroys with history of changes

* i18n: Update Polish translation (#5782)

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>

* Split admin navigation into moderation and administration

* Redesign audit log page

* 🇵🇱 (#5795)

* Add color coding to audit log

* Change dismiss->resolve, log all outcomes of report as resolve

* Update terminology (e-mail blacklist) (#5796)

* Update terminology (e-mail blacklist)

imho looks better

* Update en.yml

* Fix code style issues

* i18n-tasks normalize
This commit is contained in:
Eugen Rochko 2017-11-24 02:05:53 +01:00 committed by GitHub
parent 801eee0ff3
commit e84fecb7e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 490 additions and 43 deletions

View file

@ -21,7 +21,7 @@ module Admin
def destroy def destroy
authorize @account_moderation_note, :destroy? authorize @account_moderation_note, :destroy?
@account_moderation_note.destroy @account_moderation_note.destroy!
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end end

View file

@ -32,18 +32,21 @@ module Admin
def memorialize def memorialize
authorize @account, :memorialize? authorize @account, :memorialize?
@account.memorialize! @account.memorialize!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def enable def enable
authorize @account.user, :enable? authorize @account.user, :enable?
@account.user.enable! @account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def disable def disable
authorize @account.user, :disable? authorize @account.user, :disable?
@account.user.disable! @account.user.disable!
log_action :disable, @account.user
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
class ActionLogsController < BaseController
def index
@action_logs = Admin::ActionLog.page(params[:page])
end
end
end

View file

@ -3,6 +3,7 @@
module Admin module Admin
class BaseController < ApplicationController class BaseController < ApplicationController
include Authorization include Authorization
include AccountableConcern
before_action :require_staff! before_action :require_staff!

View file

@ -7,6 +7,7 @@ module Admin
def create def create
authorize @user, :confirm? authorize @user, :confirm?
@user.confirm! @user.confirm!
log_action :confirm, @user
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -20,6 +20,7 @@ module Admin
@custom_emoji = CustomEmoji.new(resource_params) @custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save if @custom_emoji.save
log_action :create, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else else
render :new render :new
@ -30,6 +31,7 @@ module Admin
authorize @custom_emoji, :update? authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params) if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
@ -38,7 +40,8 @@ module Admin
def destroy def destroy
authorize @custom_emoji, :destroy? authorize @custom_emoji, :destroy?
@custom_emoji.destroy @custom_emoji.destroy!
log_action :destroy, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end end
@ -49,6 +52,7 @@ module Admin
emoji.image = @custom_emoji.image emoji.image = @custom_emoji.image
if emoji.save if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
@ -60,12 +64,14 @@ module Admin
def enable def enable
authorize @custom_emoji, :enable? authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false) @custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end end
def disable def disable
authorize @custom_emoji, :disable? authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true) @custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end end

View file

@ -21,6 +21,7 @@ module Admin
if @domain_block.save if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id) DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else else
render :new render :new
@ -34,6 +35,7 @@ module Admin
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
log_action :destroy, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end

View file

@ -20,6 +20,7 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params) @email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save if @email_domain_block.save
log_action :create, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else else
render :new render :new
@ -28,7 +29,8 @@ module Admin
def destroy def destroy
authorize @email_domain_block, :destroy? authorize @email_domain_block, :destroy?
@email_domain_block.destroy @email_domain_block.destroy!
log_action :destroy, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end end

View file

@ -8,7 +8,7 @@ module Admin
def create def create
authorize :status, :update? authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
@ -16,13 +16,15 @@ module Admin
def update def update
authorize @status, :update? authorize @status, :update?
@status.update(status_params) @status.update!(status_params)
log_action :update, @status
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def destroy def destroy
authorize @status, :destroy? authorize @status, :destroy?
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status render json: @status
end end

View file

@ -25,12 +25,17 @@ module Admin
def process_report def process_report
case params[:outcome].to_s case params[:outcome].to_s
when 'resolve' when 'resolve'
@report.update(action_taken_by_current_attributes) @report.update!(action_taken_by_current_attributes)
log_action :resolve, @report
when 'suspend' when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id) Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report
log_action :suspend, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
when 'silence' when 'silence'
@report.target_account.update(silenced: true) @report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound

View file

@ -7,6 +7,7 @@ module Admin
def create def create
authorize @user, :reset_password? authorize @user, :reset_password?
@user.send_reset_password_instructions @user.send_reset_password_instructions
log_action :reset_password, @user
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -7,12 +7,14 @@ module Admin
def promote def promote
authorize @user, :promote? authorize @user, :promote?
@user.promote! @user.promote!
log_action :promote, @user
redirect_to admin_account_path(@user.account_id) redirect_to admin_account_path(@user.account_id)
end end
def demote def demote
authorize @user, :demote? authorize @user, :demote?
@user.demote! @user.demote!
log_action :demote, @user
redirect_to admin_account_path(@user.account_id) redirect_to admin_account_path(@user.account_id)
end end

View file

@ -6,13 +6,15 @@ module Admin
def create def create
authorize @account, :silence? authorize @account, :silence?
@account.update(silenced: true) @account.update!(silenced: true)
log_action :silence, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsilence? authorize @account, :unsilence?
@account.update(silenced: false) @account.update!(silenced: false)
log_action :unsilence, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -26,7 +26,7 @@ module Admin
def create def create
authorize :status, :update? authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
@ -34,13 +34,15 @@ module Admin
def update def update
authorize @status, :update? authorize @status, :update?
@status.update(status_params) @status.update!(status_params)
log_action :update, @status
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
def destroy def destroy
authorize @status, :destroy? authorize @status, :destroy?
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status render json: @status
end end

View file

@ -7,12 +7,14 @@ module Admin
def create def create
authorize @account, :suspend? authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id) Admin::SuspensionWorker.perform_async(@account.id)
log_action :suspend, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsuspend? authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -7,6 +7,7 @@ module Admin
def destroy def destroy
authorize @user, :disable_2fa? authorize @user, :disable_2fa?
@user.disable_two_factor! @user.disable_two_factor!
log_action :disable_2fa, @user
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target)
Admin::ActionLog.create(account: current_account, action: action, target: target)
end
end

View file

@ -0,0 +1,103 @@
# frozen_string_literal: true
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
end
end
def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
end
end
def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain')
elsif log.target_type == 'CustomEmoji' && log.action == :update
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
end
def log_extra_attributes(hash)
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
end
def log_change(val)
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
end
def icon_for_log(log)
case log.target_type
when 'Account', 'User'
'user'
when 'CustomEmoji'
'file'
when 'Report'
'flag'
when 'DomainBlock'
'lock'
when 'EmailDomainBlock'
'envelope'
when 'Status'
'pencil'
end
end
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize
'neutral'
when :demote, :silence, :disable, :suspend
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
else
''
end
end
private
def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
end
end

View file

@ -347,3 +347,104 @@
} }
} }
} }
.spacer {
flex: 1 1 auto;
}
.log-entry {
margin-bottom: 8px;
line-height: 20px;
&__header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px;
background: $ui-base-color;
color: $ui-primary-color;
border-radius: 4px 4px 0 0;
font-size: 14px;
position: relative;
}
&__avatar {
margin-right: 10px;
.avatar {
display: block;
margin: 0;
border-radius: 50%;
width: 40px;
height: 40px;
}
}
&__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__timestamp {
color: lighten($ui-base-color, 34%);
}
&__extras {
background: lighten($ui-base-color, 6%);
border-radius: 0 0 4px 4px;
padding: 10px;
color: $ui-primary-color;
font-family: 'mastodon-font-monospace', monospace;
font-size: 12px;
white-space: nowrap;
min-height: 20px;
}
&__icon {
font-size: 28px;
margin-right: 10px;
color: lighten($ui-base-color, 34%);
}
&__icon__overlay {
position: absolute;
top: 10px;
right: 10px;
width: 10px;
height: 10px;
border-radius: 50%;
&.positive {
background: $success-green;
}
&.negative {
background: $error-red;
}
&.neutral {
background: $ui-highlight-color;
}
}
a,
.username,
.target {
color: $ui-secondary-color;
text-decoration: none;
font-weight: 500;
}
.diff-old {
color: $error-red;
}
.diff-neutral {
color: $ui-secondary-color;
}
.diff-new {
color: $success-green;
}
}

7
app/models/admin.rb Normal file
View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Admin
def self.table_name_prefix
'admin_'
end
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: admin_action_logs
#
# id :integer not null, primary key
# account_id :integer
# action :string default(""), not null
# target_type :string
# target_id :integer
# recorded_changes :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Admin::ActionLog < ApplicationRecord
serialize :recorded_changes
belongs_to :account, required: true
belongs_to :target, required: true, polymorphic: true
default_scope -> { order('id desc') }
def action
super.to_sym
end
before_validation :set_changes
private
def set_changes
case action
when :destroy, :create
self.recorded_changes = target.attributes
when :update, :promote, :demote
self.recorded_changes = target.previous_changes
end
end
end

View file

@ -2,8 +2,9 @@
class Form::StatusBatch class Form::StatusBatch
include ActiveModel::Model include ActiveModel::Model
include AccountableConcern
attr_accessor :status_ids, :action attr_accessor :status_ids, :action, :current_account
ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
@ -20,11 +21,14 @@ class Form::StatusBatch
def change_sensitive(sensitive) def change_sensitive(sensitive)
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id) media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
ApplicationRecord.transaction do ApplicationRecord.transaction do
Status.where(id: media_attached_status_ids).find_each do |status| Status.where(id: media_attached_status_ids).find_each do |status|
status.update!(sensitive: sensitive) status.update!(sensitive: sensitive)
log_action :update, status
end end
end end
true true
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
false false
@ -33,7 +37,9 @@ class Form::StatusBatch
def delete_statuses def delete_statuses
Status.where(id: status_ids).find_each do |status| Status.where(id: status_ids).find_each do |status|
RemovalWorker.perform_async(status.id) RemovalWorker.perform_async(status.id)
log_action :destroy, status
end end
true true
end end
end end

View file

@ -0,0 +1,15 @@
%li.log-entry
.log-entry__header
.log-entry__avatar
= image_tag action_log.account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
.log-entry__content
.log-entry__title
= t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
.log-entry__timestamp
%time= l action_log.created_at
.spacer
.log-entry__icon
= fa_icon icon_for_log(action_log)
.log-entry__icon__overlay{ class: class_for_log_icon(action_log) }
.log-entry__extras
= log_extra_attributes relevant_log_changes(action_log)

View file

@ -0,0 +1,7 @@
- content_for :page_title do
= t('admin.action_logs.title')
%ul
= render @action_logs
= paginate @action_logs

View file

@ -7,10 +7,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 122, "line": 143,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).inbox_url, Account.find(params[:id]).inbox_url)", "code": "link_to(Account.find(params[:id]).inbox_url, Account.find(params[:id]).inbox_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -26,10 +26,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 128, "line": 149,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).shared_inbox_url, Account.find(params[:id]).shared_inbox_url)", "code": "link_to(Account.find(params[:id]).shared_inbox_url, Account.find(params[:id]).shared_inbox_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -45,10 +45,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 35, "line": 54,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).url, Account.find(params[:id]).url)", "code": "link_to(Account.find(params[:id]).url, Account.find(params[:id]).url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -76,6 +76,25 @@
"confidence": "Weak", "confidence": "Weak",
"note": "" "note": ""
}, },
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
"fingerprint": "4b6a895e2805578d03ceedbe1d469cc75a0c759eba093722523edb4b8683c873",
"check_name": "Render",
"message": "Render path contains parameter value",
"file": "app/views/admin/action_logs/index.html.haml",
"line": 5,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => Admin::ActionLog.page(params[:page]), {})",
"render_path": [{"type":"controller","class":"Admin::ActionLogsController","method":"index","line":7,"file":"app/controllers/admin/action_logs_controller.rb"}],
"location": {
"type": "template",
"template": "admin/action_logs/index"
},
"user_input": "params[:page]",
"confidence": "Weak",
"note": ""
},
{ {
"warning_type": "Cross-Site Scripting", "warning_type": "Cross-Site Scripting",
"warning_code": 4, "warning_code": 4,
@ -83,10 +102,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 131, "line": 152,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).followers_url, Account.find(params[:id]).followers_url)", "code": "link_to(Account.find(params[:id]).followers_url, Account.find(params[:id]).followers_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -102,10 +121,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 106, "line": 127,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).salmon_url, Account.find(params[:id]).salmon_url)", "code": "link_to(Account.find(params[:id]).salmon_url, Account.find(params[:id]).salmon_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -124,7 +143,7 @@
"line": 31, "line": 31,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})", "code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})",
"render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":9,"file":"app/controllers/admin/custom_emojis_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":10,"file":"app/controllers/admin/custom_emojis_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/custom_emojis/index" "template": "admin/custom_emojis/index"
@ -163,7 +182,7 @@
"line": 64, "line": 64,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_accounts.page(params[:page]), {})", "code": "render(action => filtered_accounts.page(params[:page]), {})",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":10,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":12,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/index" "template": "admin/accounts/index"
@ -179,10 +198,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 95, "line": 116,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).remote_url, Account.find(params[:id]).remote_url)", "code": "link_to(Account.find(params[:id]).remote_url, Account.find(params[:id]).remote_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -221,7 +240,7 @@
"line": 25, "line": 25,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_reports.page(params[:page]), {})", "code": "render(action => filtered_reports.page(params[:page]), {})",
"render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":9,"file":"app/controllers/admin/reports_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":10,"file":"app/controllers/admin/reports_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/reports/index" "template": "admin/reports/index"
@ -237,10 +256,10 @@
"check_name": "LinkToHref", "check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href", "message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml", "file": "app/views/admin/accounts/show.html.haml",
"line": 125, "line": 146,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href", "link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).outbox_url, Account.find(params[:id]).outbox_url)", "code": "link_to(Account.find(params[:id]).outbox_url, Account.find(params[:id]).outbox_url)",
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}], "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": { "location": {
"type": "template", "type": "template",
"template": "admin/accounts/show" "template": "admin/accounts/show"
@ -269,6 +288,6 @@
"note": "" "note": ""
} }
], ],
"updated": "2017-10-20 00:00:54 +0900", "updated": "2017-11-19 20:34:18 +0100",
"brakeman_version": "4.0.1" "brakeman_version": "4.0.1"
} }

View file

@ -60,3 +60,4 @@ ignore_unused:
- 'activerecord.errors.models.doorkeeper/*' - 'activerecord.errors.models.doorkeeper/*'
- 'errors.429' - 'errors.429'
- 'admin.accounts.roles.*' - 'admin.accounts.roles.*'
- 'admin.action_logs.actions.*'

View file

@ -133,6 +133,32 @@ en:
unsubscribe: Unsubscribe unsubscribe: Unsubscribe
username: Username username: Username
web: Web web: Web
action_logs:
actions:
confirm_user: "%{name} confirmed e-mail address of user %{target}"
create_custom_emoji: "%{name} uploaded new emoji %{target}"
create_domain_block: "%{name} blocked domain %{target}"
create_email_domain_block: "%{name} blacklisted e-mail domain %{target}"
demote_user: "%{name} demoted user %{target}"
destroy_domain_block: "%{name} unblocked domain %{target}"
destroy_email_domain_block: "%{name} whitelisted e-mail domain %{target}"
destroy_status: "%{name} removed status by %{target}"
disable_2fa_user: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji: "%{name} disabled emoji %{target}"
disable_user: "%{name} disabled login for user %{target}"
enable_custom_emoji: "%{name} enabled emoji %{target}"
enable_user: "%{name} enabled login for user %{target}"
memorialize_account: "%{name} turned %{target}'s account into a memoriam page"
promote_user: "%{name} promoted user %{target}"
reset_password_user: "%{name} reset password of user %{target}"
resolve_report: "%{name} dismissed report %{target}"
silence_account: "%{name} silenced %{target}'s account"
suspend_account: "%{name} suspended %{target}'s account"
unsilence_account: "%{name} unsilenced %{target}'s account"
unsuspend_account: "%{name} unsuspended %{target}'s account"
update_custom_emoji: "%{name} updated emoji %{target}"
update_status: "%{name} updated status by %{target}"
title: Audit log
custom_emojis: custom_emojis:
copied_msg: Successfully created local copy of the emoji copied_msg: Successfully created local copy of the emoji
copy: Copy copy: Copy
@ -187,24 +213,24 @@ en:
suspend: Unsuspend all existing accounts from this domain suspend: Unsuspend all existing accounts from this domain
title: Undo domain block for %{domain} title: Undo domain block for %{domain}
undo: Undo undo: Undo
title: Domain Blocks title: Domain blocks
undo: Undo undo: Undo
email_domain_blocks: email_domain_blocks:
add_new: Add new add_new: Add new
created_msg: Email domain block successfully created created_msg: Successfully added e-mail domain to blacklist
delete: Delete delete: Delete
destroyed_msg: Email domain block successfully deleted destroyed_msg: Successfully deleted e-mail domain from blacklist
domain: Domain domain: Domain
new: new:
create: Create block create: Add domain
title: New email domain block title: New e-mail blacklist entry
title: Email Domain Block title: E-mail blacklist
instances: instances:
account_count: Known accounts account_count: Known accounts
domain_name: Domain domain_name: Domain
reset: Reset reset: Reset
search: Search search: Search
title: Known Instances title: Known instances
reports: reports:
action_taken_by: Action taken by action_taken_by: Action taken by
are_you_sure: Are you sure? are_you_sure: Are you sure?
@ -265,7 +291,7 @@ en:
timeline_preview: timeline_preview:
desc_html: Display public timeline on landing page desc_html: Display public timeline on landing page
title: Timeline preview title: Timeline preview
title: Site Settings title: Site settings
statuses: statuses:
back_to_account: Back to account page back_to_account: Back to account page
batch: batch:
@ -404,6 +430,8 @@ en:
validations: validations:
images_and_video: Cannot attach a video to a status that already contains images images_and_video: Cannot attach a video to a status that already contains images
too_many: Cannot attach more than 4 files too_many: Cannot attach more than 4 files
moderation:
title: Moderation
notification_mailer: notification_mailer:
digest: digest:
body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:' body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:'

View file

@ -49,6 +49,7 @@ pl:
reserved_username: Ta nazwa użytkownika jest zarezerwowana. reserved_username: Ta nazwa użytkownika jest zarezerwowana.
roles: roles:
admin: Administrator admin: Administrator
moderator: Moderator
unfollow: Przestań śledzić unfollow: Przestań śledzić
admin: admin:
account_moderation_notes: account_moderation_notes:
@ -132,6 +133,32 @@ pl:
unsubscribe: Przestań subskrybować unsubscribe: Przestań subskrybować
username: Nazwa użytkownika username: Nazwa użytkownika
web: Sieć web: Sieć
action_logs:
actions:
confirm_user: "%{name} potwierdził adres e-mail użytkownika %{target}"
create_custom_emoji: "%{name} dodał nowe emoji %{target}"
create_domain_block: "%{name} zablokował domenę %{target}"
create_email_domain_block: "%{name} dodał domenę e-mail %{target} na czarną listę"
demote_user: "%{name} zdegradował użytkownika %{target}"
destroy_domain_block: "%{name} odblokował domenę %{target}"
destroy_email_domain_block: "%{name} usunął domenę e-mail %{target} z czarnej listy"
destroy_status: "%{name} usunął wpis użytkownika %{target}"
disable_2fa_user: "%{name} wyłączył uwierzytelnianie dwustopniowe użytkownikowi %{target}"
disable_custom_emoji: "%{name} wyłączył emoji %{target}"
disable_user: "%{name} zablokował możliwość logowania użytkownikowi %{target}"
enable_custom_emoji: "%{name} włączył emoji %{target}"
enable_user: "%{name} przywrócił możliwość logowania użytkownikowi %{target}"
memorialize_account: "%{name} nadał kontu %{target} status in memoriam"
promote_user: "%{name} podniósł uprawnienia użytkownikowi %{target}"
reset_password_user: "%{name} przywrócił hasło użytkownikowi %{target}"
resolve_report: "%{name} odrzucił zgłoszenie %{target}"
silence_account: "%{name} wyciszył konto %{target}"
suspend_account: "%{name} zawiesił konto %{target}"
unsilence_account: "%{name} cofnął wyciszenie konta %{target}"
unsuspend_account: "%{name} cofnął zawieszenie konta %{target}"
update_custom_emoji: "%{name} zaktualizował emoji %{target}"
update_status: "%{name} zaktualizował wpis użytkownika %{target}"
title: Dziennik działań administracyjnych
custom_emojis: custom_emojis:
copied_msg: Pomyślnie utworzono lokalną kopię emoji copied_msg: Pomyślnie utworzono lokalną kopię emoji
copy: Kopiuj copy: Kopiuj
@ -148,6 +175,7 @@ pl:
listed: Widoczne listed: Widoczne
new: new:
title: Dodaj nowe niestandardowe emoji title: Dodaj nowe niestandardowe emoji
overwrite: Zastąp
shortcode: Shortcode shortcode: Shortcode
shortcode_hint: Co najmniej 2 znaki, tylko znaki alfanumeryczne i podkreślniki shortcode_hint: Co najmniej 2 znaki, tylko znaki alfanumeryczne i podkreślniki
title: Niestandardowe emoji title: Niestandardowe emoji
@ -403,6 +431,8 @@ pl:
validations: validations:
images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia
too_many: Nie możesz załączyć więcej niż 4 plików too_many: Nie możesz załączyć więcej niż 4 plików
moderation:
title: Moderacja
notification_mailer: notification_mailer:
digest: digest:
body: 'Oto krótkie podsumowanie co Cię ominęło na %{instance} od Twojej ostatniej wizyty (%{since}):' body: 'Oto krótkie podsumowanie co Cię ominęło na %{instance} od Twojej ostatniej wizyty (%{since}):'

View file

@ -20,17 +20,21 @@ SimpleNavigation::Configuration.run do |navigation|
development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications} development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications}
end end
primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin| primary.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
admin.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? } admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? }
admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? } admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? }
admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } end
admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), edit_admin_settings_url, if: proc { current_user.staff? } do |admin|
admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? } admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }
admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
end end
primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' } primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }

View file

@ -110,6 +110,7 @@ Rails.application.routes.draw do
resources :subscriptions, only: [:index] resources :subscriptions, only: [:index]
resources :domain_blocks, only: [:index, :new, :create, :show, :destroy] resources :domain_blocks, only: [:index, :new, :create, :show, :destroy]
resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
resources :action_logs, only: [:index]
resource :settings, only: [:edit, :update] resource :settings, only: [:edit, :update]
resources :instances, only: [:index] do resources :instances, only: [:index] do

View file

@ -0,0 +1,12 @@
class CreateAdminActionLogs < ActiveRecord::Migration[5.1]
def change
create_table :admin_action_logs do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.string :action, null: false, default: ''
t.references :target, polymorphic: true
t.text :recorded_changes, null: false, default: ''
t.timestamps
end
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171118012443) do ActiveRecord::Schema.define(version: 20171119172437) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -80,6 +80,18 @@ ActiveRecord::Schema.define(version: 20171118012443) do
t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true
end end
create_table "admin_action_logs", force: :cascade do |t|
t.bigint "account_id"
t.string "action", default: "", null: false
t.string "target_type"
t.bigint "target_id"
t.text "recorded_changes", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
end
create_table "blocks", force: :cascade do |t| create_table "blocks", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -488,6 +500,7 @@ ActiveRecord::Schema.define(version: 20171118012443) do
add_foreign_key "account_moderation_notes", "accounts" add_foreign_key "account_moderation_notes", "accounts"
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade

View file

@ -0,0 +1,5 @@
Fabricator('Admin::ActionLog') do
account nil
action "MyString"
target nil
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Admin::ActionLog, type: :model do
end