diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 61445344e0c22..841561291f28d 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 343af9ebdb334..8fdb7df32731f 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 18d9eef8408c2..bcfc5e66dbd32 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'); - }, -); - -Rails.delegate(document, '#ng-words-table .add-row-button', 'click', (ev) => { - ev.preventDefault(); - addTableRow('ng-words-table'); -}); + removeTableRow(ev.target, id); + }); +}; -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 6f5af9d1f071f..5b7d6569e2efc 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -98,10 +98,10 @@ def approve_remote! 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 326434a843ffa..171bac8975cb3 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 0000000000000..d7e2ef115ef91 --- /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 ce7c23e5679d7..6897ef3068297 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -142,11 +142,7 @@ def set_immediate_attributes! 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 0000000000000..d9eb8dfae6178 --- /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 a91540be23aaa..fc4f354e53ab8 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 1331836acba55..a96b9e335342f 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 21f42aa225ef1..9c7ad5ebf1e80 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 ca9802e6e9ba7..c211b2306ab88 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 b6180e167ad84..d71ee5eab9f84 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 0000000000000..c053ecb6599e3 --- /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 deaca692cd207..f3b6bfc9453ee 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 @@ 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 0b3a476537e1b..cd2da0677e4ac 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 0000000000000..1f18fc7c71a99 --- /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 796bcb425e546..f801227f57c13 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -13,7 +13,7 @@ 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 @@ 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 @@ 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 @@ 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