From ff2860d0df19b3654d080db9ddc2828d612de381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?= Date: Wed, 3 Apr 2024 12:09:43 +0900 Subject: [PATCH] =?UTF-8?q?Change:=20#591=20=E3=83=9B=E3=83=AF=E3=82=A4?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=82=B9=E3=83=88=E3=81=AE=E3=83=89=E3=83=A1?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E4=B8=80=E8=A6=A7=E3=81=AE=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=85=88=E3=83=BB=E7=94=BB=E9=9D=A2=E5=A4=89=E6=9B=B4=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change: #591 ホワイトリストのドメイン一覧の保存先・画面変更 * Update account_batch.rb * 表示まわりを改善 * Update dangerous.rake --- .haml-lint_todo.yml | 1 + .../admin/ng_words/white_list_controller.rb | 23 ++++++ app/javascript/packs/admin.tsx | 41 +++------- app/models/form/account_batch.rb | 6 +- app/models/form/admin_settings.rb | 2 - app/models/specified_domain.rb | 78 +++++++++++++++++++ .../activitypub/process_account_service.rb | 6 +- .../white_list/_specified_domain.html.haml | 6 ++ .../admin/ng_words/white_list/show.html.haml | 20 ++++- config/locales/en.yml | 4 +- config/locales/ja.yml | 4 +- config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + ...20240401222541_create_specified_domains.rb | 41 ++++++++++ db/schema.rb | 11 ++- lib/tasks/dangerous.rake | 2 + .../specified_domain_fabricator.rb | 5 ++ .../process_account_service_spec.rb | 8 +- 18 files changed, 211 insertions(+), 49 deletions(-) create mode 100644 app/models/specified_domain.rb create mode 100644 app/views/admin/ng_words/white_list/_specified_domain.html.haml create mode 100644 db/migrate/20240401222541_create_specified_domains.rb create mode 100644 spec/fabricators/specified_domain_fabricator.rb diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 61445344e0..841561291f 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -32,4 +32,5 @@ linters: - 'app/views/application/_sidebar.html.haml' - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - 'app/views/admin/ng_words/keywords/_ng_word.html.haml' + - 'app/views/admin/ng_words/white_list/_specified_domain.html.haml' - 'app/views/admin/sensitive_words/_sensitive_word.html.haml' diff --git a/app/controllers/admin/ng_words/white_list_controller.rb b/app/controllers/admin/ng_words/white_list_controller.rb index 343af9ebdb..8fdb7df327 100644 --- a/app/controllers/admin/ng_words/white_list_controller.rb +++ b/app/controllers/admin/ng_words/white_list_controller.rb @@ -2,10 +2,33 @@ module Admin class NgWords::WhiteListController < NgWordsController + def show + super + @white_list_domains = SpecifiedDomain.white_list_domain_caches.presence || [SpecifiedDomain.new] + end + protected + def validate + begin + SpecifiedDomain.save_from_raws_as_white_list(settings_params_list) + return true + rescue + flash[:alert] = I18n.t('admin.ng_words.save_error') + redirect_to after_update_redirect_path + end + + false + end + def after_update_redirect_path admin_ng_words_white_list_path end + + private + + def settings_params_list + params.require(:form_admin_settings)[:specified_domains] + end end end diff --git a/app/javascript/packs/admin.tsx b/app/javascript/packs/admin.tsx index 18d9eef840..bcfc5e66db 100644 --- a/app/javascript/packs/admin.tsx +++ b/app/javascript/packs/admin.tsx @@ -316,40 +316,21 @@ const removeTableRow = (target: EventTarget | null, tableId: string) => { tableElement.removeChild(tableRowElement); }; -Rails.delegate( - document, - '#sensitive-words-table .add-row-button', - 'click', - (ev) => { +const setupTableList = (id: string) => { + Rails.delegate(document, `#${id} .add-row-button`, 'click', (ev) => { ev.preventDefault(); - addTableRow('sensitive-words-table'); - }, -); + addTableRow(id); + }); -Rails.delegate( - document, - '#sensitive-words-table .delete-row-button', - 'click', - (ev) => { + Rails.delegate(document, `#${id} .delete-row-button`, 'click', (ev) => { ev.preventDefault(); - removeTableRow(ev.target, 'sensitive-words-table'); - }, -); + removeTableRow(ev.target, id); + }); +}; -Rails.delegate(document, '#ng-words-table .add-row-button', 'click', (ev) => { - ev.preventDefault(); - addTableRow('ng-words-table'); -}); - -Rails.delegate( - document, - '#ng-words-table .delete-row-button', - 'click', - (ev) => { - ev.preventDefault(); - removeTableRow(ev.target, 'ng-words-table'); - }, -); +setupTableList('sensitive-words-table'); +setupTableList('ng-words-table'); +setupTableList('white-list-table'); async function mountReactComponent(element: Element) { const componentName = element.getAttribute('data-admin-component'); diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 6f5af9d1f0..5b7d6569e2 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -98,10 +98,10 @@ class Form::AccountBatch def approve_remote_domain! domains = accounts.group_by(&:domain).pluck(0) - if (Setting.permit_new_account_domains || []).compact_blank.present? - list = ((Setting.permit_new_account_domains || []) + domains).compact_blank.uniq.join("\n") - Form::AdminSettings.new(permit_new_account_domains: list).save + (domains - SpecifiedDomain.where(domain: domains, table: 0).pluck(:domain)).each do |domain| + SpecifiedDomain.create!(domain: domain, table: 0) end + Account.where(domain: domains, remote_pending: true).find_each do |account| approve_remote_account(account) end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 326434a843..171bac8975 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -61,7 +61,6 @@ class Form::AdminSettings unlocked_friend enable_local_timeline emoji_reaction_disallow_domains - permit_new_account_domains block_unfollow_account_mention hold_remote_new_accounts ).freeze @@ -121,7 +120,6 @@ class Form::AdminSettings STRING_ARRAY_KEYS = %i( emoji_reaction_disallow_domains - permit_new_account_domains ).freeze attr_accessor(*KEYS) diff --git a/app/models/specified_domain.rb b/app/models/specified_domain.rb new file mode 100644 index 0000000000..d7e2ef115e --- /dev/null +++ b/app/models/specified_domain.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: specified_domains +# +# id :bigint(8) not null, primary key +# domain :string not null +# table :integer default(0), not null +# options :jsonb not null +# created_at :datetime not null +# updated_at :datetime not null +# +class SpecifiedDomain < ApplicationRecord + attr_accessor :domains + + validates :domain, uniqueness: { scope: :table } + after_commit :invalidate_cache! + + scope :white_list_domains, -> { where(table: 0) } + + class << self + def white_list_domain_caches + Rails.cache.fetch('specified_domains:white_list') { white_list_domains.to_a } + end + + def save_from_hashes(rows, type, caches) + unmatched = caches + matched = [] + + SpecifiedDomain.transaction do + rows.filter { |item| item[:domain].present? }.each do |item| + exists = unmatched.find { |i| i.domain == item[:domain] } + + if exists.present? + unmatched.delete(exists) + matched << exists + + next unless item.key?(:options) && item[:options] == exists.options + + exists.update!(options: item[:options]) + elsif matched.none? { |i| i.domain == item[:domain] } + SpecifiedDomain.create!( + domain: item[:domain], + table: type, + options: item[:options] || {} + ) + end + end + + SpecifiedDomain.destroy(unmatched.map(&:id)) + end + + true + end + + def save_from_raws(rows, type, caches) + hashes = (rows['domains'] || []).map do |domain| + { + domain: domain, + type: type, + } + end + + save_from_hashes(hashes, type, caches) + end + + def save_from_raws_as_white_list(rows) + save_from_raws(rows, 0, white_list_domain_caches) + end + end + + private + + def invalidate_cache! + Rails.cache.delete('specified_domains:white_list') + end +end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index ce7c23e567..6897ef3068 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -142,11 +142,7 @@ class ActivityPub::ProcessAccountService < BaseService def blocking_new_account? return false unless Setting.hold_remote_new_accounts - permit_new_account_domains.exclude?(@domain) - end - - def permit_new_account_domains - (Setting.permit_new_account_domains || []).compact_blank + SpecifiedDomain.white_list_domain_caches.none? { |item| item.domain == @domain } end def valid_account? diff --git a/app/views/admin/ng_words/white_list/_specified_domain.html.haml b/app/views/admin/ng_words/white_list/_specified_domain.html.haml new file mode 100644 index 0000000000..d9eb8dfae6 --- /dev/null +++ b/app/views/admin/ng_words/white_list/_specified_domain.html.haml @@ -0,0 +1,6 @@ +- temporary_id = defined?(@temp_id) ? @temp_id += 1 : @temp_id = 1 +%tr{ class: template ? 'template-row' : nil } + %td= f.input :domains, as: :string, input_html: { multiple: true, value: specified_domain.domain } + %td + = hidden_field_tag :'form_admin_settings[specified_domains][temporary_ids][]', temporary_id, class: 'temporary_id' + = link_to safe_join([fa_icon('times'), t('filters.index.delete')]), '#', class: 'table-action-link delete-row-button' diff --git a/app/views/admin/ng_words/white_list/show.html.haml b/app/views/admin/ng_words/white_list/show.html.haml index a91540be23..fc4f354e53 100644 --- a/app/views/admin/ng_words/white_list/show.html.haml +++ b/app/views/admin/ng_words/white_list/show.html.haml @@ -18,8 +18,24 @@ .fields-group = f.input :hold_remote_new_accounts, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hold_remote_new_accounts'), hint: t('admin.ng_words.remote_approval_hint') - .fields-group - = f.input :permit_new_account_domains, wrapper: :with_label, as: :text, kmyblue: true, input_html: { rows: 6 }, label: t('admin.ng_words.permit_new_account_domains') + %h4= t('admin.ng_words.white_list_header') + + .table-wrapper + %table.table.keywords-table#white-list-table + %thead + %tr + %th= t('simple_form.labels.defaults.domain') + %th + %tbody + = f.simple_fields_for :specified_domains, @white_list_domains do |domain| + = render partial: 'specified_domain', collection: @white_list_domains, locals: { f: domain, template: false } + + = f.simple_fields_for :specified_domains, @white_list_domains do |domain| + = render partial: 'specified_domain', collection: [SpecifiedDomain.new], locals: { f: domain, template: true } + %tfoot + %tr + %td{ colspan: 2 } + = link_to safe_join([fa_icon('plus'), t('admin.ng_words.edit.add_domain')]), '#', class: 'table-action-link add-row-button' .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 1331836acb..a96b9e3353 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -749,11 +749,12 @@ en: block_unfollow_account_mention_hint: この設定は削除予定です。設定削除後は、常にチェックをつけていない場合と同じ挙動になります。NGルールで代替してください。 deprecated: Will remove settings deprecated_hint: These settings will be removed in the next LTS or kmyblue version 14.0, whichever comes first. Please refer to the description of each setting and replace them with the new settings if possible. + edit: + add_domain: Add domain hide_local_users_for_anonymous: Hide timeline local user posts from anonymous hide_local_users_for_anonymous_hint: この設定は削除予定です。設定削除後は、常にチェックをつけていない場合と同じ挙動になります。サーバー設定の「見つける」にある「公開タイムラインへの未認証のアクセスを許可する」で、完全ではありませんが代替可能です。 hold_remote_new_accounts: Hold new remote accounts keywords: Reject keywords - permit_new_account_domains: Domain list to automatically approve new users preamble: This setting is useful for solving problems related to spam that are difficult to address with domain blocking. You can reject posts that meet certain criteria, such as the inclusion of specific keywords. Please consider your settings carefully and check your history regularly to ensure that problem-free submissions are not deleted. post_hash_tags_max: Hash tags max for posts post_mentions_max: Mentions max for posts @@ -771,6 +772,7 @@ en: test_error: Testing is returned any errors title: NG words and against spams white_list: White list + white_list_header: List of domains for immediate approval of remote accounts white_list_hint: Whitelisting can be used as a last resort when exposed to severe attacks. External attacks do not disappear immediately, but they can be reduced gradually and reliably through moderation. In addition, a regular remote account approval process is required. ngword_histories: back_to_ng_words: NG words and against spams diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 21f42aa225..9c7ad5ebf1 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -748,11 +748,12 @@ ja: block_unfollow_account_mention_hint: この設定は削除予定です。設定削除後は、常にチェックをつけていない場合と同じ挙動になります。NGルールで代替してください。 deprecated: 新しいバージョンで削除する予定の設定 deprecated_hint: これらの設定は、次回のLTS、またはkmyblueバージョン14.0のどちらか早い方で削除する予定です。それぞれの設定の説明を参照して、可能であれば新しい設定に置き換えてください。 + edit: + add_domain: ドメインを追加 hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする hide_local_users_for_anonymous_hint: この設定は削除予定です。設定削除後は、常にチェックをつけていない場合と同じ挙動になります。サーバー設定の「見つける」にある「公開タイムラインへの未認証のアクセスを許可する」で、完全ではありませんが代替可能です。 hold_remote_new_accounts: リモートの新規アカウントを保留する keywords: 拒否するキーワード - permit_new_account_domains: 新規ユーザーを自動承認するドメイン phrases: regexp_html: 正規 表現 にチェックの入っている項目は、正規表現を用いての比較となります。 regexp_short: 正規 @@ -770,6 +771,7 @@ ja: test_error: NGワードのテストに失敗しました。正規表現のミスが含まれているかもしれません title: NGワードとスパム white_list: ホワイトリスト + white_list_header: リモートアカウントを即座に承認するドメイン一覧 white_list_hint: 激しい攻撃に晒された場合の最終手段として、ホワイトリストが利用できます。外部からの攻撃が即座に消えるわけではありませんが、モデレーションを進めることで徐々に確実に減らすことができます。また、定期的なリモートアカウント承認作業が求められます。 ngword_histories: back_to_ng_words: NGワードとスパム diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index ca9802e6e9..c211b2306a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -218,6 +218,7 @@ en: discoverable: Suggest account to others discoverable_local: Disallow suggesting account on other servers display_name: Display name + domain: Domain email: E-mail address expires_in: Expire after fields: Extra fields diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index b6180e167a..d71ee5eab9 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -229,6 +229,7 @@ ja: discoverable: ディレクトリに掲載する discoverable_local: 他サーバーのディレクトリに掲載しない display_name: 表示名 + domain: ドメイン email: メールアドレス expires_in: 有効期限 fields: プロフィール補足情報 diff --git a/db/migrate/20240401222541_create_specified_domains.rb b/db/migrate/20240401222541_create_specified_domains.rb new file mode 100644 index 0000000000..c053ecb659 --- /dev/null +++ b/db/migrate/20240401222541_create_specified_domains.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CreateSpecifiedDomains < ActiveRecord::Migration[7.1] + class Setting < ApplicationRecord + def value + YAML.safe_load(self[:value], permitted_classes: [ActiveSupport::HashWithIndifferentAccess, Symbol]) if self[:value].present? + end + + def value=(new_value) + self[:value] = new_value.to_yaml + end + end + + class SpecifiedDomain < ApplicationRecord; end + + def up + create_table :specified_domains do |t| + t.string :domain, null: false + t.integer :table, default: 0, null: false + t.jsonb :options, null: false, default: {} + + t.timestamps + end + + add_index :specified_domains, %i(domain table), unique: true + + setting = Setting.find_by(var: :permit_new_account_domains) + + (setting&.value || []).compact.uniq.each do |domain| + SpecifiedDomain.create!(domain: domain, table: 0) + end + setting&.destroy + end + + def down + Setting.find_by(var: :permit_new_account_domains)&.destroy + Setting.new(var: :permit_new_account_domains).tap { |s| s.value = SpecifiedDomain.where(table: 0).pluck(:domain) }.save! + + drop_table :specified_domains + end +end diff --git a/db/schema.rb b/db/schema.rb index deaca692cd..f3b6bfc945 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_27_234026) do +ActiveRecord::Schema[7.1].define(version: 2024_04_01_222541) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1328,6 +1328,15 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_27_234026) do t.index ["version"], name: "index_software_updates_on_version", unique: true end + create_table "specified_domains", force: :cascade do |t| + t.string "domain", null: false + t.integer "table", default: 0, null: false + t.jsonb "options", default: {}, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["domain", "table"], name: "index_specified_domains_on_domain_and_table", unique: true + end + create_table "status_capability_tokens", force: :cascade do |t| t.bigint "status_id", null: false t.string "token" diff --git a/lib/tasks/dangerous.rake b/lib/tasks/dangerous.rake index 0b3a476537..cd2da0677e 100644 --- a/lib/tasks/dangerous.rake +++ b/lib/tasks/dangerous.rake @@ -97,6 +97,7 @@ namespace :dangerous do 20240320231633 20240326231854 20240327234026 + 20240401222541 ) # Removed: account_groups target_tables = %w( @@ -121,6 +122,7 @@ namespace :dangerous do pending_statuses scheduled_expiration_statuses sensitive_words + specified_domains status_capability_tokens status_references ) diff --git a/spec/fabricators/specified_domain_fabricator.rb b/spec/fabricators/specified_domain_fabricator.rb new file mode 100644 index 0000000000..1f18fc7c71 --- /dev/null +++ b/spec/fabricators/specified_domain_fabricator.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Fabricator(:specified_domain) do + domain { sequence(:domain) { |i| "example_#{i}.com" } } +end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 796bcb425e..f801227f57 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -13,7 +13,7 @@ RSpec.describe ActivityPub::ProcessAccountService do subject { described_class.new.call('alice', 'example.com', payload) } let(:hold_remote_new_accounts) { true } - let(:permit_new_account_domains) { nil } + let(:permit_domain) { nil } let(:payload) do { id: 'https://foo.test', @@ -26,7 +26,7 @@ RSpec.describe ActivityPub::ProcessAccountService do before do Setting.hold_remote_new_accounts = hold_remote_new_accounts - Setting.permit_new_account_domains = permit_new_account_domains + Fabricate(:specified_domain, domain: permit_domain, table: 0) if permit_domain end it 'creates pending account in a simple case' do @@ -37,7 +37,7 @@ RSpec.describe ActivityPub::ProcessAccountService do end context 'when is blocked' do - let(:permit_new_account_domains) { ['foo.bar'] } + let(:permit_domain) { 'foo.bar' } it 'creates pending account' do expect(subject).to_not be_nil @@ -98,7 +98,7 @@ RSpec.describe ActivityPub::ProcessAccountService do end context 'when is in whitelist' do - let(:permit_new_account_domains) { ['example.com'] } + let(:permit_domain) { 'example.com' } it 'does not create account' do expect(subject).to_not be_nil