Compare commits

...

5 Commits

Author SHA1 Message Date
Takeshi Umeda
07c9accec8
Merge b23596109f into 94bceb8683 2025-07-11 14:03:35 +00:00
noellabo
b23596109f Fix admin_user 2025-05-08 23:15:57 +09:00
noellabo
75707d05a9 Add spec activity/create 2025-05-08 23:15:57 +09:00
noellabo
c1f8763e9b Delete unuse svg file 2025-05-08 23:15:57 +09:00
noellabo
129dd5bd9c Add reject pattern to Admin setting 2025-05-08 23:15:57 +09:00
14 changed files with 171 additions and 1 deletions

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Settings::ProtectionsController < Admin::SettingsController
private
def after_update_redirect_path
admin_settings_protections_path
end
end

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q97-30 162-118.5T718-480H480v-315l-240 90v207q0 7 2 18h238v316Z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@ -14,7 +14,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
private private
def create_status def create_status
return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? || reject_pattern?
with_redis_lock("create:#{object_uri}") do with_redis_lock("create:#{object_uri}") do
return if delete_arrived_first?(object_uri) || poll_vote? return if delete_arrived_first?(object_uri) || poll_vote?
@ -450,6 +450,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
Tombstone.exists?(uri: object_uri) Tombstone.exists?(uri: object_uri)
end end
def reject_pattern?
Setting.reject_pattern.present? && @object['content']&.match?(Setting.reject_pattern)
end
def forward_for_reply def forward_for_reply
return unless @status.distributable? && @json['signature'].present? && reply_to_local? return unless @status.distributable? && @json['signature'].present? && reply_to_local?

View File

@ -41,6 +41,7 @@ class Form::AdminSettings
app_icon app_icon
favicon favicon
min_age min_age
reject_pattern
).freeze ).freeze
INTEGER_KEYS = %i( INTEGER_KEYS = %i(
@ -94,6 +95,7 @@ class Form::AdminSettings
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
validates :reject_pattern, regexp_syntax: true, if: -> { defined?(@reject_pattern) }
validates :status_page_url, url: true, allow_blank: true validates :status_page_url, url: true, allow_blank: true
validate :validate_site_uploads validate :validate_site_uploads

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RegexpSyntaxValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
Regexp.compile(value)
rescue RegexpError => e
record.errors.add(attribute, I18n.t('applications.invalid_regexp', message: e.message))
end
end

View File

@ -0,0 +1,22 @@
- content_for :page_title do
= t('admin.settings.protections.title')
- content_for :heading do
%h2= t('admin.settings.title')
= render partial: 'admin/settings/shared/links'
= simple_form_for @admin_settings, url: admin_settings_protections_path, html: { method: :patch } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.settings.protections.preamble')
%h4= t('admin.settings.protections.activitypub')
.fields-group
= f.input :reject_pattern,
as: :text,
input_html: { rows: 8 },
wrapper: :with_block_label
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View File

@ -7,3 +7,4 @@
primary.item :discovery, safe_join([material_symbol('search'), t('admin.settings.discovery.title')]), admin_settings_discovery_path primary.item :discovery, safe_join([material_symbol('search'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
primary.item :content_retention, safe_join([material_symbol('history'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path primary.item :content_retention, safe_join([material_symbol('history'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
primary.item :appearance, safe_join([material_symbol('computer'), t('admin.settings.appearance.title')]), admin_settings_appearance_path primary.item :appearance, safe_join([material_symbol('computer'), t('admin.settings.appearance.title')]), admin_settings_appearance_path
primary.item :protections, safe_join([material_symbol('security'), t('admin.settings.protections.title')]), admin_settings_protections_path

View File

@ -842,6 +842,10 @@ en:
all: To everyone all: To everyone
disabled: To no one disabled: To no one
users: To logged-in local users users: To logged-in local users
protections:
activitypub: ActivityPub
preamble: Settings to protect your server
title: Protection
registrations: registrations:
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone! moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
preamble: Control who can create an account on your server. preamble: Control who can create an account on your server.
@ -1178,6 +1182,7 @@ en:
applications: applications:
created: Application successfully created created: Application successfully created
destroyed: Application successfully deleted destroyed: Application successfully deleted
invalid_regexp: 'The provided Regexp is invalid: %{message}'
logout: Logout logout: Logout
regenerate_token: Regenerate access token regenerate_token: Regenerate access token
token_regenerated: Access token successfully regenerated token_regenerated: Access token successfully regenerated

View File

@ -94,6 +94,7 @@ en:
min_age: Users will be asked to confirm their date of birth during sign-up min_age: Users will be asked to confirm their date of birth during sign-up
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
profile_directory: The profile directory lists all users who have opted-in to be discoverable. profile_directory: The profile directory lists all users who have opted-in to be discoverable.
reject_pattern: Set a regular expression pattern to inspect Create Activity content, and refuse Activity if you match
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
site_contact_email: How people can reach you for legal or support inquiries. site_contact_email: How people can reach you for legal or support inquiries.
site_contact_username: How people can reach you on Mastodon. site_contact_username: How people can reach you on Mastodon.
@ -285,6 +286,7 @@ en:
peers_api_enabled: Publish list of discovered servers in the API peers_api_enabled: Publish list of discovered servers in the API
profile_directory: Enable profile directory profile_directory: Enable profile directory
registrations_mode: Who can sign-up registrations_mode: Who can sign-up
reject_pattern: Reject Pattern
require_invite_text: Require a reason to join require_invite_text: Require a reason to join
show_domain_blocks: Show domain blocks show_domain_blocks: Show domain blocks
show_domain_blocks_rationale: Show why domains were blocked show_domain_blocks_rationale: Show why domains were blocked

View File

@ -68,6 +68,7 @@ namespace :admin do
resource :about, only: [:show, :update], controller: 'about' resource :about, only: [:show, :update], controller: 'about'
resource :appearance, only: [:show, :update], controller: 'appearance' resource :appearance, only: [:show, :update], controller: 'appearance'
resource :discovery, only: [:show, :update], controller: 'discovery' resource :discovery, only: [:show, :update], controller: 'discovery'
resource :protections, only: [:show, :update], controller: 'protections'
end end
resources :site_uploads, only: [:destroy] resources :site_uploads, only: [:destroy]

View File

@ -52,6 +52,7 @@ defaults: &defaults
backups_retention_period: 7 backups_retention_period: 7
captcha_enabled: false captcha_enabled: false
allow_referer_origin: false allow_referer_origin: false
reject_pattern: ''
development: development:
<<: *defaults <<: *defaults

View File

@ -26,6 +26,8 @@ RSpec.describe ActivityPub::Activity::Create do
before do before do
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
Setting.reject_pattern = 'spam'
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' }) stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
@ -95,6 +97,24 @@ RSpec.describe ActivityPub::Activity::Create do
} }
end end
let(:spam_object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post3'].join('/'),
type: 'Note',
to: [
'https://www.w3.org/ns/activitystreams#Public',
ActivityPub::TagManager.instance.uri_for(follower),
],
content: '@bob lorem ispam',
published: 1.hour.ago.utc.iso8601,
updated: 1.hour.ago.utc.iso8601,
tag: {
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(follower),
},
}
end
def activity_for_object(json) def activity_for_object(json)
{ {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
@ -168,6 +188,17 @@ RSpec.describe ActivityPub::Activity::Create do
# It has queued a mention resolve job # It has queued a mention resolve job
expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything) expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything)
end end
it 'do not process posts that contain reject patterns', :aggregate_failures do
stub_request(:get, spam_object_json[:id]).to_return(status: 500)
described_class.new(activity_for_object(spam_object_json), sender, delivery: true).perform
# …it creates a status
status = Status.find_by(uri: spam_object_json[:id])
# Check the process did crash
expect(status.nil?).to be true
end
end end
describe '#perform' do describe '#perform' do

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin::Settings::Protections' do
let(:admin_user) { Fabricate(:admin_user) }
before { sign_in(admin_user) }
it 'Saves changes to protections settings' do
visit admin_settings_protections_path
fill_in reject_pattern_field,
with: 'https://foo.bar'
click_on submit_button
expect(page)
.to have_content(success_message)
end
def reject_pattern_field
form_label 'form_admin_settings.reject_pattern'
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe RegexpSyntaxValidator do
let(:record_class) do
Class.new do
include ActiveModel::Validations
attr_accessor :text
validates :text, regexp_syntax: true
end
end
let(:record) { record_class.new }
describe '#validate_each' do
context 'with a nil value' do
it 'does not add errors' do
record.text = nil
expect(record).to be_valid
expect(record.errors).to be_empty
end
end
context 'with a blank' do
it 'does not add errors' do
record.text = ''
expect(record).to be_valid
expect(record.errors).to be_empty
end
end
context 'with valid regexp' do
it 'does not add errors' do
record.text = 'spam|https://foo.bar'
expect(record).to be_valid
expect(record.errors).to be_empty
end
end
context 'with more lines than limit' do
it 'adds an error' do
record.text = '('
expect(record).to_not be_valid
expect(record.errors.where(:text)).to_not be_empty
end
end
end
end