From f7f23b4a19a84371f44ec5297125e96ba81681a1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 19 Jun 2019 23:42:38 +0200 Subject: Add audio uploads (#11123) * Add audio uploads Fix #4827 Accept uploads of OGG, WAV, FLAC, OPUS and MP3 files, and converts them to OGG. Media attachments get a new `audio` type. In the UI, audio uploads are displayed identically to video uploads. * Improve code style --- app/controllers/media_controller.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index a245db2d1..d44b52d26 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -7,6 +7,8 @@ class MediaController < ApplicationController before_action :set_media_attachment before_action :verify_permitted_status! + before_action :check_playable, only: :player + before_action :allow_iframing, only: :player content_security_policy only: :player do |p| p.frame_ancestors(false) @@ -18,8 +20,6 @@ class MediaController < ApplicationController def player @body_classes = 'player' - response.headers['X-Frame-Options'] = 'ALLOWALL' - raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end private @@ -34,4 +34,12 @@ class MediaController < ApplicationController # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end + + def check_playable + not_found unless @media_attachment.larger_media_format? + end + + def allow_iframing + response.headers['X-Frame-Options'] = 'ALLOWALL' + end end -- cgit From 33144e132d28f5b820ae12e4b8e4fb34ca47b1d6 Mon Sep 17 00:00:00 2001 From: "Acid Chicken (硫酸鶏)" Date: Thu, 20 Jun 2019 09:18:06 +0900 Subject: Fix layout of identity proofs settings (#11126) --- app/controllers/settings/identity_proofs_controller.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index e22b4d9be..a749d8020 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -56,8 +56,4 @@ class Settings::IdentityProofsController < Settings::BaseController def post_params params.require(:account_identity_proof).permit(:post_status, :status_text) end - - def set_body_classes - @body_classes = '' - end end -- cgit From 7696f77245c2302787d239da50248385b3292a5e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 20 Jun 2019 02:52:34 +0200 Subject: Add moderation API (#9387) Fix #8580 Fix #7143 --- app/controllers/admin/accounts_controller.rb | 1 + .../api/v1/admin/account_actions_controller.rb | 32 +++++ .../api/v1/admin/accounts_controller.rb | 128 ++++++++++++++++++ app/controllers/api/v1/admin/reports_controller.rb | 108 +++++++++++++++ app/models/account.rb | 2 + app/models/account_filter.rb | 2 + app/models/concerns/user_roles.rb | 14 ++ app/models/report.rb | 3 + app/models/report_filter.rb | 2 + app/models/user.rb | 1 + app/serializers/rest/admin/account_serializer.rb | 77 +++++++++++ app/serializers/rest/admin/report_serializer.rb | 16 +++ config/initializers/doorkeeper.rb | 8 +- config/locales/doorkeeper.en.yml | 6 + config/routes.rb | 23 ++++ .../v1/admin/account_actions_controller_spec.rb | 57 ++++++++ .../api/v1/admin/accounts_controller_spec.rb | 147 +++++++++++++++++++++ .../api/v1/admin/reports_controller_spec.rb | 109 +++++++++++++++ 18 files changed, 735 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/admin/account_actions_controller.rb create mode 100644 app/controllers/api/v1/admin/accounts_controller.rb create mode 100644 app/controllers/api/v1/admin/reports_controller.rb create mode 100644 app/serializers/rest/admin/account_serializer.rb create mode 100644 app/serializers/rest/admin/report_serializer.rb create mode 100644 spec/controllers/api/v1/admin/account_actions_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/accounts_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/reports_controller_spec.rb (limited to 'app/controllers') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index b0d45ce47..0c7760d77 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -127,6 +127,7 @@ module Admin :by_domain, :active, :pending, + :disabled, :silenced, :suspended, :username, diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb new file mode 100644 index 000000000..29c9b7107 --- /dev/null +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountActionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } + before_action :require_staff! + before_action :set_account + + def create + account_action = Admin::AccountAction.new(resource_params) + account_action.target_account = @account + account_action.current_account = current_account + account_action.save! + + render_empty + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def resource_params + params.permit( + :type, + :report_id, + :warning_preset_id, + :text, + :send_email_notification + ) + end +end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb new file mode 100644 index 000000000..c306180ca --- /dev/null +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] + before_action :require_staff! + before_action :set_accounts, only: :index + before_action :set_account, except: :index + before_action :require_local_account!, only: [:enable, :approve, :reject] + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + local + remote + by_domain + active + pending + disabled + silenced + suspended + username + display_name + email + ip + staff + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :account, :index? + render json: @accounts, each_serializer: REST::Admin::AccountSerializer + end + + def show + authorize @account, :show? + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + log_action :enable, @account.user + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def approve + authorize @account.user, :approve? + @account.user.approve! + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsilence + authorize @account, :unsilence? + @account.unsilence! + log_action :unsilence, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsuspend + authorize @account, :unsuspend? + @account.unsuspend! + log_action :unsuspend, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + private + + def set_accounts + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_account + @account = Account.find(params[:id]) + end + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def require_local_account! + forbidden unless @account.local? && @account.user.present? + end +end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb new file mode 100644 index 000000000..1d48d3160 --- /dev/null +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Api::V1::Admin::ReportsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] + before_action :require_staff! + before_action :set_reports, only: :index + before_action :set_report, except: :index + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + resolved + account_id + target_account_id + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :report, :index? + render json: @reports, each_serializer: REST::Admin::ReportSerializer + end + + def show + authorize @report, :show? + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def assign_to_self + authorize @report, :update? + @report.update!(assigned_account_id: current_account.id) + log_action :assigned_to_self, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def unassign + authorize @report, :update? + @report.update!(assigned_account_id: nil) + log_action :unassigned, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def reopen + authorize @report, :update? + @report.unresolve! + log_action :reopen, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def resolve + authorize @report, :update? + @report.resolve!(current_account) + log_action :resolve, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + private + + def set_reports + @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_report + @report = Report.find(params[:id]) + end + + def filtered_reports + ReportFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? + end + + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id + end + + def records_continue? + @reports.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index c977f887c..9276aa927 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -106,6 +106,8 @@ class Account < ApplicationRecord :confirmed?, :approved?, :pending?, + :disabled?, + :role, :admin?, :moderator?, :staff?, diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index d2503100c..c3b1fe08d 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -37,6 +37,8 @@ class AccountFilter Account.without_suspended when 'pending' accounts_with_users.merge User.pending + when 'disabled' + accounts_with_users.merge User.disabled when 'silenced' Account.silenced when 'suspended' diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb index 58dffdc46..a42b4a172 100644 --- a/app/models/concerns/user_roles.rb +++ b/app/models/concerns/user_roles.rb @@ -13,6 +13,20 @@ module UserRoles admin? || moderator? end + def role=(value) + case value + when 'admin' + self.admin = true + self.moderator = false + when 'moderator' + self.admin = false + self.moderator = true + else + self.admin = false + self.moderator = false + end + end + def role if admin? 'admin' diff --git a/app/models/report.rb b/app/models/report.rb index 86c303798..5192ceef7 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -17,6 +17,8 @@ # class Report < ApplicationRecord + include Paginable + belongs_to :account belongs_to :target_account, class_name: 'Account' belongs_to :action_taken_by_account, class_name: 'Account', optional: true @@ -26,6 +28,7 @@ class Report < ApplicationRecord scope :unresolved, -> { where(action_taken: false) } scope :resolved, -> { where(action_taken: true) } + scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].each_with_object({}) { |k, h| h[k] = { user: [:invite_request, :invite] } }) } validates :comment, length: { maximum: 1000 } diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index 56ab28df7..a392d60c3 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -9,9 +9,11 @@ class ReportFilter def results scope = Report.unresolved + params.each do |key, value| scope = scope.merge scope_for(key, value) end + scope end diff --git a/app/models/user.rb b/app/models/user.rb index 4abf124fc..50873dd01 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -87,6 +87,7 @@ class User < ApplicationRecord scope :approved, -> { where(approved: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } + scope :disabled, -> { where(disabled: true) } 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_at: nil }) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb new file mode 100644 index 000000000..f579d3302 --- /dev/null +++ b/app/serializers/rest/admin/account_serializer.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class REST::Admin::AccountSerializer < ActiveModel::Serializer + attributes :id, :username, :domain, :created_at, + :email, :ip, :role, :confirmed, :suspended, + :silenced, :disabled, :approved, :locale, + :invite_request + + attribute :created_by_application_id, if: :created_by_application? + attribute :invited_by_account_id, if: :invited? + + has_one :account, serializer: REST::AccountSerializer + + def id + object.id.to_s + end + + def email + object.user_email + end + + def ip + object.user_current_sign_in_ip.to_s.presence + end + + def role + object.user_role + end + + def suspended + object.suspended? + end + + def silenced + object.silenced? + end + + def confirmed + object.user_confirmed? + end + + def disabled + object.user_disabled? + end + + def approved + object.user_approved? + end + + def account + object + end + + def locale + object.user_locale + end + + def created_by_application_id + object.user&.created_by_application_id&.to_s&.presence + end + + def invite_request + object.user&.invite_request&.text + end + + def invited_by_account_id + object.user&.invite&.user&.account_id&.to_s&.presence + end + + def invited? + object.user&.invited? + end + + def created_by_application? + object.user&.created_by_application_id&.present? + end +end diff --git a/app/serializers/rest/admin/report_serializer.rb b/app/serializers/rest/admin/report_serializer.rb new file mode 100644 index 000000000..7a77132c0 --- /dev/null +++ b/app/serializers/rest/admin/report_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class REST::Admin::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken, :comment, :created_at, :updated_at + + has_one :account, serializer: REST::Admin::AccountSerializer + has_one :target_account, serializer: REST::Admin::AccountSerializer + has_one :assigned_account, serializer: REST::Admin::AccountSerializer + has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer + + has_many :statuses, serializer: REST::StatusSerializer + + def id + object.id.to_s + end +end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 367eead6a..914b3c001 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -80,7 +80,13 @@ Doorkeeper.configure do :'read:search', :'read:statuses', :follow, - :push + :push, + :'admin:read', + :'admin:read:accounts', + :'admin:read:reports', + :'admin:write', + :'admin:write:accounts', + :'admin:write:reports' # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index f1fe03716..d9b7c2c8e 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -114,6 +114,12 @@ en: application: title: OAuth authorization required scopes: + admin:read: read all data on the server + admin:read:accounts: read sensitive information of all accounts + admin:read:reports: read sensitive information of all reports and reported accounts + admin:write: modify all data on the server + admin:write:accounts: perform moderation actions on accounts + admin:write:reports: perform moderation actions on reports follow: modify account relationships push: receive your push notifications read: read all your account's data diff --git a/config/routes.rb b/config/routes.rb index 145079c69..764db8db2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -398,6 +398,29 @@ Rails.application.routes.draw do namespace :push do resource :subscription, only: [:create, :show, :update, :destroy] end + + namespace :admin do + resources :accounts, only: [:index, :show] do + member do + post :enable + post :unsilence + post :unsuspend + post :approve + post :reject + end + + resource :action, only: [:create], controller: 'account_actions' + end + + resources :reports, only: [:index, :show] do + member do + post :assign_to_self + post :unassign + post :reopen + post :resolve + end + end + end end namespace :v2 do diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb new file mode 100644 index 000000000..a5a8f4bb0 --- /dev/null +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'POST #create' do + before do + post :create, params: { account_id: account.id, type: 'disable' } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'performs action against account' do + expect(account.reload.user_disabled?).to be true + end + + it 'logs action' do + log_item = Admin::ActionLog.last + + expect(log_item).to_not be_nil + expect(log_item.action).to eq :disable + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end +end diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f3f9946ba --- /dev/null +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -0,0 +1,147 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #approve' do + before do + account.user.update(approved: false) + post :approve, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'approves user' do + expect(account.reload.user_approved?).to be true + end + end + + describe 'POST #reject' do + before do + account.user.update(approved: false) + post :reject, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes user' do + expect(User.where(id: account.user.id).count).to eq 0 + end + end + + describe 'POST #enable' do + before do + account.user.update(disabled: true) + post :enable, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'enables user' do + expect(account.reload.user_disabled?).to be false + end + end + + describe 'POST #unsuspend' do + before do + account.touch(:suspended_at) + post :unsuspend, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsuspends account' do + expect(account.reload.suspended?).to be false + end + end + + describe 'POST #unsilence' do + before do + account.touch(:silenced_at) + post :unsilence, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsilences account' do + expect(account.reload.silenced?).to be false + end + end +end diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb new file mode 100644 index 000000000..4ed3c5dc4 --- /dev/null +++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::ReportsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:report) { Fabricate(:report) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #resolve' do + before do + post :resolve, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #reopen' do + before do + post :reopen, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #assign_to_self' do + before do + post :assign_to_self, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #unassign' do + before do + post :unassign, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end -- cgit From 707ddf7808f90e3ab042d7642d368c2ce8e95e6f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 22 Jun 2019 00:13:10 +0200 Subject: Change domain blocks to automatically support subdomains (#11138) * Change domain blocks to automatically support subdomains If a more authoritative domain is blocked (example.com), then the same block will be applied to a subdomain (foo.example.com) * Match subdomains of existing accounts when blocking/unblocking domains * Improve code style --- app/controllers/admin/domain_blocks_controller.rb | 2 +- app/controllers/admin/instances_controller.rb | 2 +- app/controllers/media_proxy_controller.rb | 2 +- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/activity/flag.rb | 2 +- app/lib/ostatus/activity/creation.rb | 4 +-- app/models/account.rb | 1 + app/models/custom_emoji.rb | 1 + app/models/domain_block.rb | 33 ++++++++++++++++++++-- app/models/instance.rb | 2 +- .../activitypub/process_account_service.rb | 2 +- app/services/block_domain_service.rb | 4 +-- app/services/resolve_account_service.rb | 2 +- app/services/unblock_domain_service.rb | 3 +- app/services/update_remote_profile_service.rb | 4 +-- spec/models/account_spec.rb | 17 +++++++++++ spec/models/domain_block_spec.rb | 31 +++++++++++++++----- 17 files changed, 89 insertions(+), 25 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 71597763b..377cac8ad 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -13,7 +13,7 @@ module Admin authorize :domain_block, :create? @domain_block = DomainBlock.new(resource_params) - existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil + existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 6dd659a30..7888e844f 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -18,7 +18,7 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.find_by(domain: params[:id]) + @domain_block = DomainBlock.rule_for(params[:id]) end private diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 950cf6d09..8fc18dd06 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -39,6 +39,6 @@ class MediaProxyController < ApplicationController end def reject_media? - DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + DomainBlock.reject_media?(@media_attachment.account.domain) end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index f55dd35b2..487e8e91e 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -380,7 +380,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def skip_download? return @skip_download if defined?(@skip_download) - @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? + @skip_download ||= DomainBlock.reject_media?(@account.domain) end def reply_to_local? diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index f73b93058..1659bc61f 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -23,7 +23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity private def skip_reports? - DomainBlock.find_by(domain: @account.domain)&.reject_reports? + DomainBlock.reject_reports?(@account.domain) end def object_uris diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 3840c8fbf..60de712db 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -148,7 +148,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def save_media - do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(@account.domain) media_attachments = [] @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| @@ -176,7 +176,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def save_emojis(parent) - do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(parent.account.domain) return if do_not_download diff --git a/app/models/account.rb b/app/models/account.rb index 9276aa927..c588451fc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -98,6 +98,7 @@ class Account < ApplicationRecord scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } scope :popular, -> { order('account_stats.followers_count desc') } + scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } delegate :email, :unconfirmed_email, diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index d3cc70504..e73cd9bd2 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -39,6 +39,7 @@ class CustomEmoji < ApplicationRecord scope :local, -> { where(domain: nil) } scope :remote, -> { where.not(domain: nil) } scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) } + scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } remotable_attachment :image, LIMIT diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 84c08c158..25d3b87ef 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -24,14 +24,41 @@ class DomainBlock < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } - def self.blocked?(domain) - where(domain: domain, severity: :suspend).exists? + class << self + def suspend?(domain) + !!rule_for(domain)&.suspend? + end + + def silence?(domain) + !!rule_for(domain)&.silence? + end + + def reject_media?(domain) + !!rule_for(domain)&.reject_media? + end + + def reject_reports?(domain) + !!rule_for(domain)&.reject_reports? + end + + alias blocked? suspend? + + def rule_for(domain) + return if domain.blank? + + uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') } + segments = uri.normalized_host.split('.') + variants = segments.map.with_index { |_, i| segments[i..-1].join('.') } + + where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first + end end def stricter_than?(other_block) - return true if suspend? + return true if suspend? return false if other_block.suspend? && (silence? || noop?) return false if other_block.silence? && noop? + (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) end diff --git a/app/models/instance.rb b/app/models/instance.rb index 7bf000d40..a01db1212 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -8,7 +8,7 @@ class Instance def initialize(resource) @domain = resource.domain @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count - @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain) + @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain) end def cached_sample_accounts diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index ad22d37fe..05c017bdf 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -205,7 +205,7 @@ class ActivityPub::ProcessAccountService < BaseService def domain_block return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.find_by(domain: @domain) + @domain_block = DomainBlock.rule_for(@domain) end def key_changed? diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 497f0394b..c6eef04d4 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -76,7 +76,7 @@ class BlockDomainService < BaseService end def blocked_domain_accounts - Account.where(domain: blocked_domain) + Account.by_domain_and_subdomains(blocked_domain) end def media_from_blocked_domain @@ -84,6 +84,6 @@ class BlockDomainService < BaseService end def emojis_from_blocked_domains - CustomEmoji.where(domain: blocked_domain) + CustomEmoji.by_domain_and_subdomains(blocked_domain) end end diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 11e33a83a..57c9ccfe1 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -146,7 +146,7 @@ class ResolveAccountService < BaseService def domain_block return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.find_by(domain: @domain) + @domain_block = DomainBlock.rule_for(@domain) end def atom_url diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb index 9b8526fbe..fc262a50a 100644 --- a/app/services/unblock_domain_service.rb +++ b/app/services/unblock_domain_service.rb @@ -14,7 +14,8 @@ class UnblockDomainService < BaseService end def blocked_accounts - scope = Account.where(domain: domain_block.domain) + scope = Account.by_domain_and_subdomains(domain_block.domain) + if domain_block.silence? scope.where(silenced_at: @domain_block.created_at) else diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index 68d36addf..403395a0d 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -26,7 +26,7 @@ class UpdateRemoteProfileService < BaseService account.note = remote_profile.note || '' account.locked = remote_profile.locked? - if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media? + if !account.suspended? && !DomainBlock.reject_media?(account.domain) if remote_profile.avatar.present? account.avatar_remote_url = remote_profile.avatar else @@ -46,7 +46,7 @@ class UpdateRemoteProfileService < BaseService end def save_emojis - do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(account.domain) return if do_not_download diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 379872316..ce9ea250d 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -687,6 +687,23 @@ RSpec.describe Account, type: :model do end end + describe 'by_domain_and_subdomains' do + it 'returns exact domain matches' do + account = Fabricate(:account, domain: 'example.com') + expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + end + + it 'returns subdomains' do + account = Fabricate(:account, domain: 'foo.example.com') + expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + end + + it 'does not return partially matching domains' do + account = Fabricate(:account, domain: 'grexample.com') + expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account] + end + end + describe 'expiring' do it 'returns remote accounts with followers whose subscription expiration date is past or not given' do local = Fabricate(:account, domain: nil) diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 0035fd0ff..d98c5e118 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -21,23 +21,40 @@ RSpec.describe DomainBlock, type: :model do end end - describe 'blocked?' do + describe '.blocked?' do it 'returns true if the domain is suspended' do - Fabricate(:domain_block, domain: 'domain', severity: :suspend) - expect(DomainBlock.blocked?('domain')).to eq true + Fabricate(:domain_block, domain: 'example.com', severity: :suspend) + expect(DomainBlock.blocked?('example.com')).to eq true end it 'returns false even if the domain is silenced' do - Fabricate(:domain_block, domain: 'domain', severity: :silence) - expect(DomainBlock.blocked?('domain')).to eq false + Fabricate(:domain_block, domain: 'example.com', severity: :silence) + expect(DomainBlock.blocked?('example.com')).to eq false end it 'returns false if the domain is not suspended nor silenced' do - expect(DomainBlock.blocked?('domain')).to eq false + expect(DomainBlock.blocked?('example.com')).to eq false end end - describe 'stricter_than?' do + describe '.rule_for' do + it 'returns rule matching a blocked domain' do + block = Fabricate(:domain_block, domain: 'example.com') + expect(DomainBlock.rule_for('example.com')).to eq block + end + + it 'returns a rule matching a subdomain of a blocked domain' do + block = Fabricate(:domain_block, domain: 'example.com') + expect(DomainBlock.rule_for('sub.example.com')).to eq block + end + + it 'returns a rule matching a blocked subdomain' do + block = Fabricate(:domain_block, domain: 'sub.example.com') + expect(DomainBlock.rule_for('sub.example.com')).to eq block + end + end + + describe '#stricter_than?' do it 'returns true if the new block has suspend severity while the old has lower severity' do suspend = DomainBlock.new(domain: 'domain', severity: :suspend) silence = DomainBlock.new(domain: 'domain', severity: :silence) -- cgit