Add moderator role and add pundit policies for admin actions (#5635)

* Add moderator role and add pundit policies for admin actions

* Add rake task for turning user into mod and revoking it again

* Fix handling of unauthorized exception

* Deliver new report e-mails to staff, not just admins

* Add promote/demote to admin UI, hide some actions conditionally

* Fix unused i18n
This commit is contained in:
Eugen Rochko 2017-11-11 20:23:33 +01:00 committed by GitHub
parent 2b1190065c
commit 7bb8b0b2fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 539 additions and 91 deletions

View file

@ -1,31 +1,41 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::AccountModerationNotesController < Admin::BaseController module Admin
def create class AccountModerationNotesController < BaseController
@account_moderation_note = current_account.account_moderation_notes.new(resource_params) before_action :set_account_moderation_note, only: [:destroy]
if @account_moderation_note.save
@target_account = @account_moderation_note.target_account def create
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg') authorize AccountModerationNote, :create?
else
@account = @account_moderation_note.target_account @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show' if @account_moderation_note.save
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show'
end
end
def destroy
authorize @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')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
def set_account_moderation_note
@account_moderation_note = AccountModerationNote.find(params[:id])
end end
end end
def destroy
@account_moderation_note = AccountModerationNote.find(params[:id])
@target_account = @account_moderation_note.target_account
@account_moderation_note.destroy
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
end end

View file

@ -7,40 +7,49 @@ module Admin
before_action :require_local_account!, only: [:enable, :disable, :memorialize] before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index def index
authorize :account, :index?
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
end end
def show def show
authorize @account, :show?
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
end end
def subscribe def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id) Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def unsubscribe def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def memorialize def memorialize
authorize @account, :memorialize?
@account.memorialize! @account.memorialize!
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def enable def enable
authorize @account.user, :enable?
@account.user.enable! @account.user.enable!
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def disable def disable
authorize @account.user, :disable?
@account.user.disable! @account.user.disable!
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def redownload def redownload
authorize @account, :redownload?
@account.reset_avatar! @account.reset_avatar!
@account.reset_header! @account.reset_header!
@account.save! @account.save!

View file

@ -2,7 +2,9 @@
module Admin module Admin
class BaseController < ApplicationController class BaseController < ApplicationController
before_action :require_admin! include Authorization
before_action :require_staff!
layout 'admin' layout 'admin'
end end

View file

@ -2,15 +2,18 @@
module Admin module Admin
class ConfirmationsController < BaseController class ConfirmationsController < BaseController
before_action :set_user
def create def create
account_user.confirm authorize @user, :confirm?
@user.confirm!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def account_user def set_user
Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end
end end
end end

View file

@ -5,14 +5,18 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_custom_emoji, except: [:index, :new, :create]
def index def index
authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
end end
def new def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new @custom_emoji = CustomEmoji.new
end end
def create def create
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new(resource_params) @custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save if @custom_emoji.save
@ -23,6 +27,8 @@ module Admin
end end
def update def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params) if @custom_emoji.update(resource_params)
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
@ -31,11 +37,14 @@ module Admin
end end
def destroy def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy @custom_emoji.destroy
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
def copy def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode) emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode)
if emoji.update(image: @custom_emoji.image) if emoji.update(image: @custom_emoji.image)
@ -48,11 +57,13 @@ module Admin
end end
def enable def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false) @custom_emoji.update!(disabled: false)
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?
@custom_emoji.update!(disabled: true) @custom_emoji.update!(disabled: true)
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

@ -5,14 +5,18 @@ module Admin
before_action :set_domain_block, only: [:show, :destroy] before_action :set_domain_block, only: [:show, :destroy]
def index def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page]) @domain_blocks = DomainBlock.page(params[:page])
end end
def new def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new @domain_block = DomainBlock.new
end end
def create def create
authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
if @domain_block.save if @domain_block.save
@ -23,9 +27,12 @@ module Admin
end end
end end
def show; end def show
authorize @domain_block, :show?
end
def destroy def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
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

@ -5,14 +5,18 @@ module Admin
before_action :set_email_domain_block, only: [:show, :destroy] before_action :set_email_domain_block, only: [:show, :destroy]
def index def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page]) @email_domain_blocks = EmailDomainBlock.page(params[:page])
end end
def new def new
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new @email_domain_block = EmailDomainBlock.new
end end
def create def create
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new(resource_params) @email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save if @email_domain_block.save
@ -23,6 +27,7 @@ module Admin
end end
def destroy def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy @email_domain_block.destroy
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

@ -3,10 +3,12 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
def index def index
authorize :instance, :index?
@instances = ordered_instances @instances = ordered_instances
end end
def resubscribe def resubscribe
authorize :instance, :resubscribe?
params.require(:by_domain) params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path redirect_to admin_instances_path

View file

@ -2,19 +2,20 @@
module Admin module Admin
class ReportedStatusesController < BaseController class ReportedStatusesController < BaseController
include Authorization
before_action :set_report before_action :set_report
before_action :set_status, only: [:update, :destroy] before_action :set_status, only: [:update, :destroy]
def create def create
@form = Form::StatusBatch.new(form_status_batch_params) authorize :status, :update?
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View file

@ -5,14 +5,17 @@ module Admin
before_action :set_report, except: [:index] before_action :set_report, except: [:index]
def index def index
authorize :report, :index?
@reports = filtered_reports.page(params[:page]) @reports = filtered_reports.page(params[:page])
end end
def show def show
authorize @report, :show?
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def update def update
authorize @report, :update?
process_report process_report
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View file

@ -2,17 +2,18 @@
module Admin module Admin
class ResetsController < BaseController class ResetsController < BaseController
before_action :set_account before_action :set_user
def create def create
@account.user.send_reset_password_instructions authorize @user, :reset_password?
@user.send_reset_password_instructions
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_account def set_user
@account = Account.find(params[:account_id]) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end
end end
end end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Admin
class RolesController < BaseController
before_action :set_user
def promote
authorize @user, :promote?
@user.promote!
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
redirect_to admin_account_path(@user.account_id)
end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -28,10 +28,13 @@ module Admin
).freeze ).freeze
def edit def edit
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end
def update def update
authorize :settings, :update?
settings_params.each do |key, value| settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key) if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key) upload = SiteUpload.where(var: key).first_or_initialize(var: key)

View file

@ -5,11 +5,13 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :silence?
@account.update(silenced: true) @account.update(silenced: true)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsilence?
@account.update(silenced: false) @account.update(silenced: false)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -2,8 +2,6 @@
module Admin module Admin
class StatusesController < BaseController class StatusesController < BaseController
include Authorization
helper_method :current_params helper_method :current_params
before_action :set_account before_action :set_account
@ -12,24 +10,30 @@ module Admin
PER_PAGE = 20 PER_PAGE = 20
def index def index
authorize :status, :index?
@statuses = @account.statuses @statuses = @account.statuses
if params[:media] if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids)) @statuses.merge!(Status.where(id: account_media_status_ids))
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end end
def create def create
@form = Form::StatusBatch.new(form_status_batch_params) authorize :status, :update?
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
@form = Form::StatusBatch.new(form_status_batch_params)
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)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
@ -60,6 +64,7 @@ module Admin
def current_params def current_params
page = (params[:page] || 1).to_i page = (params[:page] || 1).to_i
{ {
media: params[:media], media: params[:media],
page: page > 1 && page, page: page > 1 && page,

View file

@ -3,6 +3,7 @@
module Admin module Admin
class SubscriptionsController < BaseController class SubscriptionsController < BaseController
def index def index
authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page) @subscriptions = ordered_subscriptions.page(requested_page)
end end

View file

@ -5,11 +5,13 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id) Admin::SuspensionWorker.perform_async(@account.id)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

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

View file

@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment] comment: report_params[:comment]
) )
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer render json: @report, serializer: REST::ReportSerializer
end end

View file

@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in? before_action :check_suspension, if: :user_signed_in?
@ -40,6 +41,10 @@ class ApplicationController < ActionController::Base
redirect_to root_path unless current_user&.admin? redirect_to root_path unless current_user&.admin?
end end
def require_staff!
redirect_to root_path unless current_user&.staff?
end
def check_suspension def check_suspension
forbidden if current_user.account.suspended? forbidden if current_user.account.suspended?
end end

View file

@ -2,6 +2,7 @@
module Authorization module Authorization
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Pundit include Pundit
def pundit_user def pundit_user

View file

@ -35,6 +35,11 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)" Rails.env.production? ? site_title : "#{site_title} (Dev)"
end end
def can?(action, record)
return false if record.nil?
policy(record).public_send("#{action}?")
end
def fa_icon(icon, attributes = {}) def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || [] class_names = attributes[:class]&.split(' ') || []
class_names << 'fa' class_names << 'fa'

View file

@ -32,6 +32,7 @@
# filtered_languages :string default([]), not null, is an Array # filtered_languages :string default([]), not null, is an Array
# account_id :integer not null # account_id :integer not null
# disabled :boolean default(FALSE), not null # disabled :boolean default(FALSE), not null
# moderator :boolean default(FALSE), not null
# #
class User < ApplicationRecord class User < ApplicationRecord
@ -53,8 +54,10 @@ class User < ApplicationRecord
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
validates_with BlacklistedEmailValidator, if: :email_changed? validates_with BlacklistedEmailValidator, if: :email_changed?
scope :recent, -> { order(id: :desc) } scope :recent, -> { order(id: :desc) }
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :moderators, -> { where(moderator: true) }
scope :staff, -> { admins.or(moderators) }
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) }
@ -74,6 +77,20 @@ class User < ApplicationRecord
confirmed_at.present? confirmed_at.present?
end end
def staff?
admin? || moderator?
end
def role
if admin?
'admin'
elsif moderator?
'moderator'
else
'user'
end
end
def disable! def disable!
update!(disabled: true, update!(disabled: true,
last_sign_in_at: current_sign_in_at, last_sign_in_at: current_sign_in_at,
@ -84,6 +101,27 @@ class User < ApplicationRecord
update!(disabled: false) update!(disabled: false)
end end
def confirm!
skip_confirmation!
save!
end
def promote!
if moderator?
update!(moderator: false, admin: true)
elsif !admin?
update!(moderator: true)
end
end
def demote!
if admin?
update!(admin: false, moderator: true)
elsif moderator?
update!(moderator: false)
end
end
def disable_two_factor! def disable_two_factor!
self.otp_required_for_login = false self.otp_required_for_login = false
otp_backup_codes&.clear otp_backup_codes&.clear

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AccountModerationNotePolicy < ApplicationPolicy
def create?
staff?
end
def destroy?
admin? || owner?
end
private
def owner?
record.account_id == current_account&.id
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class AccountPolicy < ApplicationPolicy
def index?
staff?
end
def show?
staff?
end
def suspend?
staff? && !record.user&.staff?
end
def unsuspend?
staff?
end
def silence?
staff? && !record.user&.staff?
end
def unsilence?
staff?
end
def redownload?
admin?
end
def subscribe?
admin?
end
def unsubscribe?
admin?
end
def memorialize?
admin? && !record.user&.admin?
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :current_account, :record
def initialize(current_account, record)
@current_account = current_account
@record = record
end
delegate :admin?, :moderator?, :staff?, to: :current_user, allow_nil: true
private
def current_user
current_account&.user
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
class CustomEmojiPolicy < ApplicationPolicy
def index?
staff?
end
def create?
admin?
end
def update?
admin?
end
def copy?
admin?
end
def enable?
staff?
end
def disable?
staff?
end
def destroy?
admin?
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class DomainBlockPolicy < ApplicationPolicy
def index?
admin?
end
def show?
admin?
end
def create?
admin?
end
def destroy?
admin?
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class EmailDomainBlockPolicy < ApplicationPolicy
def index?
admin?
end
def create?
admin?
end
def destroy?
admin?
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class InstancePolicy < ApplicationPolicy
def index?
admin?
end
def resubscribe?
admin?
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class ReportPolicy < ApplicationPolicy
def update?
staff?
end
def index?
staff?
end
def show?
staff?
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class SettingsPolicy < ApplicationPolicy
def update?
admin?
end
def show?
admin?
end
end

View file

@ -1,20 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusPolicy class StatusPolicy < ApplicationPolicy
attr_reader :account, :status def index?
staff?
def initialize(account, status)
@account = account
@status = status
end end
def show? def show?
if direct? if direct?
owned? || status.mentions.where(account: account).exists? owned? || record.mentions.where(account: current_account).exists?
elsif private? elsif private?
owned? || account&.following?(status.account) || status.mentions.where(account: account).exists? owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists?
else else
account.nil? || !status.account.blocking?(account) current_account.nil? || !author.blocking?(current_account)
end end
end end
@ -23,26 +20,30 @@ class StatusPolicy
end end
def destroy? def destroy?
admin? || owned? staff? || owned?
end end
alias unreblog? destroy? alias unreblog? destroy?
private def update?
staff?
def admin?
account&.user&.admin?
end end
private
def direct? def direct?
status.direct_visibility? record.direct_visibility?
end end
def owned? def owned?
status.account.id == account&.id author.id == current_account&.id
end end
def private? def private?
status.private_visibility? record.private_visibility?
end
def author
record.account
end end
end end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class SubscriptionPolicy < ApplicationPolicy
def index?
admin?
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
class UserPolicy < ApplicationPolicy
def reset_password?
staff? && !record.staff?
end
def disable_2fa?
admin? && !record.staff?
end
def confirm?
staff? && !record.confirmed?
end
def enable?
admin?
end
def disable?
admin? && !record.admin?
end
def promote?
admin? && promoteable?
end
def demote?
admin? && !record.admin? && demoteable?
end
private
def promoteable?
!record.staff? || !record.admin?
end
def demoteable?
record.staff?
end
end

View file

@ -7,4 +7,4 @@
%time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) }
= l account_moderation_note.created_at = l account_moderation_note.created_at
%td %td
= link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note)

View file

@ -17,16 +17,20 @@
- if @account.local? - if @account.local?
%tr %tr
%th= t('admin.accounts.email') %th= t('admin.accounts.email')
%td= @account.user_email %td
= @account.user_email
- if @account.user_confirmed?
= fa_icon('check')
%tr %tr
%th= t('admin.accounts.login_status') %th= t('admin.accounts.login_status')
%td %td
- if @account.user&.disabled? - if @account.user&.disabled?
= t('admin.accounts.disabled') = t('admin.accounts.disabled')
= table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
- else - else
= t('admin.accounts.enabled') = t('admin.accounts.enabled')
= table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post if can?(:disable, @account.user)
%tr %tr
%th= t('admin.accounts.most_recent_ip') %th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip %td= @account.user_current_sign_in_ip
@ -71,28 +75,28 @@
%div{ style: 'overflow: hidden' } %div{ style: 'overflow: hidden' }
%div{ style: 'float: right' } %div{ style: 'float: right' }
- if @account.local? - if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login? - if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- unless @account.memorial? - unless @account.memorial?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:memorialize, @account)
- else - else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
%div{ style: 'float: left' } %div{ style: 'float: left' }
- if @account.silenced? - if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' if can?(:unsilence, @account)
- else - else
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' if can?(:silence, @account)
- if @account.local? - if @account.local?
- unless @account.user_confirmed? - unless @account.user_confirmed?
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
- if @account.suspended? - if @account.suspended?
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' if can?(:unsuspend, @account)
- else - else
= link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account)
- unless @account.local? - unless @account.local?
%hr %hr
@ -118,9 +122,9 @@
%div{ style: 'overflow: hidden' } %div{ style: 'overflow: hidden' }
%div{ style: 'float: right' } %div{ style: 'float: right' }
= link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' if can?(:subscribe, @account)
- if @account.subscribed? - if @account.subscribed?
= link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
%hr %hr
%h3 ActivityPub %h3 ActivityPub
@ -141,6 +145,20 @@
%th= t('admin.accounts.followers_url') %th= t('admin.accounts.followers_url')
%td= link_to @account.followers_url, @account.followers_url %td= link_to @account.followers_url, @account.followers_url
- else
%hr
.table-wrapper
%table.table
%tbody
%tr
%th= t('admin.accounts.role')
%td
= t("admin.accounts.roles.#{@account.user&.role}")
%td<
= table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
= table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
%hr %hr
%h3= t('admin.accounts.moderation_notes') %h3= t('admin.accounts.moderation_notes')

View file

@ -46,6 +46,7 @@ ignore_missing:
- 'terms.body_html' - 'terms.body_html'
- 'application_mailer.salutation' - 'application_mailer.salutation'
- 'errors.500' - 'errors.500'
ignore_unused: ignore_unused:
- 'activemodel.errors.*' - 'activemodel.errors.*'
- 'activerecord.attributes.*' - 'activerecord.attributes.*'
@ -58,3 +59,4 @@ ignore_unused:
- 'errors.messages.*' - 'errors.messages.*'
- 'activerecord.errors.models.doorkeeper/*' - 'activerecord.errors.models.doorkeeper/*'
- 'errors.429' - 'errors.429'
- 'admin.accounts.roles.*'

View file

@ -62,6 +62,7 @@ en:
by_domain: Domain by_domain: Domain
confirm: Confirm confirm: Confirm
confirmed: Confirmed confirmed: Confirmed
demote: Demote
disable: Disable disable: Disable
disable_two_factor_authentication: Disable 2FA disable_two_factor_authentication: Disable 2FA
disabled: Disabled disabled: Disabled
@ -101,6 +102,7 @@ en:
outbox_url: Outbox URL outbox_url: Outbox URL
perform_full_suspension: Perform full suspension perform_full_suspension: Perform full suspension
profile_url: Profile URL profile_url: Profile URL
promote: Promote
protocol: Protocol protocol: Protocol
public: Public public: Public
push_subscription_expires: PuSH subscription expires push_subscription_expires: PuSH subscription expires
@ -108,6 +110,11 @@ en:
reset: Reset reset: Reset
reset_password: Reset password reset_password: Reset password
resubscribe: Resubscribe resubscribe: Resubscribe
role: Permissions
roles:
admin: Administrator
moderator: Moderator
user: User
salmon_url: Salmon URL salmon_url: Salmon URL
search: Search search: Search
shared_inbox_url: Shared Inbox URL shared_inbox_url: Shared Inbox URL

View file

@ -20,16 +20,16 @@ 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.admin? } do |admin| primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
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} 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 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} 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} 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' } 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' } admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url 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}
end end

View file

@ -137,6 +137,13 @@ Rails.application.routes.draw do
resource :suspension, only: [:create, :destroy] resource :suspension, only: [:create, :destroy]
resource :confirmation, only: [:create] resource :confirmation, only: [:create]
resources :statuses, only: [:index, :create, :update, :destroy] resources :statuses, only: [:index, :create, :update, :destroy]
resource :role do
member do
post :promote
post :demote
end
end
end end
resources :users, only: [] do resources :users, only: [] do

View file

@ -0,0 +1,15 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddModeratorToAccounts < ActiveRecord::Migration[5.1]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def up
safety_assured { add_column_with_default :users, :moderator, :bool, default: false }
end
def down
remove_column :users, :moderator
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: 20171107143624) do ActiveRecord::Schema.define(version: 20171109012327) 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"
@ -437,6 +437,7 @@ ActiveRecord::Schema.define(version: 20171107143624) do
t.string "filtered_languages", default: [], null: false, array: true t.string "filtered_languages", default: [], null: false, array: true
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.boolean "disabled", default: false, null: false t.boolean "disabled", default: false, null: false
t.boolean "moderator", default: false, null: false
t.index ["account_id"], name: "index_users_on_account_id" t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true

View file

@ -10,14 +10,41 @@ namespace :mastodon do
desc 'Turn a user into an admin, identified by the USERNAME environment variable' desc 'Turn a user into an admin, identified by the USERNAME environment variable'
task make_admin: :environment do task make_admin: :environment do
include RoutingHelper include RoutingHelper
account_username = ENV.fetch('USERNAME') account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username }) user = User.joins(:account).where(accounts: { username: account_username })
if user.present? if user.present?
user.update(admin: true) user.update(admin: true)
puts "Congrats! #{account_username} is now an admin. \\o/\nNavigate to #{edit_admin_settings_url} to get started" puts "Congrats! #{account_username} is now an admin. \\o/\nNavigate to #{edit_admin_settings_url} to get started"
else else
puts "User could not be found; please make sure an Account with the `#{account_username}` username exists." puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end
end
desc 'Turn a user into a moderator, identified by the USERNAME environment variable'
task make_mod: :environment do
account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username })
if user.present?
user.update(moderator: true)
puts "Congrats! #{account_username} is now a moderator \\o/"
else
puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end
end
desc 'Remove admin and moderator privileges from user identified by the USERNAME environment variable'
task revoke_staff: :environment do
account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username })
if user.present?
user.update(moderator: false, admin: false)
puts "#{account_username} is no longer admin or moderator."
else
puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end end
end end