Browse Source

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
Eugen Rochko 1 month ago
parent
commit
707ddf7808
No account linked to committer's email address

+ 1
- 1
app/controllers/admin/domain_blocks_controller.rb View File

@@ -13,7 +13,7 @@ module Admin
13 13
       authorize :domain_block, :create?
14 14
 
15 15
       @domain_block = DomainBlock.new(resource_params)
16
-      existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
16
+      existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
17 17
 
18 18
       if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
19 19
         @domain_block.save

+ 1
- 1
app/controllers/admin/instances_controller.rb View File

@@ -18,7 +18,7 @@ module Admin
18 18
       @blocks_count    = Block.where(target_account: Account.where(domain: params[:id])).count
19 19
       @available       = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
20 20
       @media_storage   = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
21
-      @domain_block    = DomainBlock.find_by(domain: params[:id])
21
+      @domain_block    = DomainBlock.rule_for(params[:id])
22 22
     end
23 23
 
24 24
     private

+ 1
- 1
app/controllers/media_proxy_controller.rb View File

@@ -39,6 +39,6 @@ class MediaProxyController < ApplicationController
39 39
   end
40 40
 
41 41
   def reject_media?
42
-    DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
42
+    DomainBlock.reject_media?(@media_attachment.account.domain)
43 43
   end
44 44
 end

+ 1
- 1
app/lib/activitypub/activity/create.rb View File

@@ -380,7 +380,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
380 380
 
381 381
   def skip_download?
382 382
     return @skip_download if defined?(@skip_download)
383
-    @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
383
+    @skip_download ||= DomainBlock.reject_media?(@account.domain)
384 384
   end
385 385
 
386 386
   def reply_to_local?

+ 1
- 1
app/lib/activitypub/activity/flag.rb View File

@@ -23,7 +23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
23 23
   private
24 24
 
25 25
   def skip_reports?
26
-    DomainBlock.find_by(domain: @account.domain)&.reject_reports?
26
+    DomainBlock.reject_reports?(@account.domain)
27 27
   end
28 28
 
29 29
   def object_uris

+ 2
- 2
app/lib/ostatus/activity/creation.rb View File

@@ -148,7 +148,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
148 148
   end
149 149
 
150 150
   def save_media
151
-    do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
151
+    do_not_download = DomainBlock.reject_media?(@account.domain)
152 152
     media_attachments = []
153 153
 
154 154
     @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
@@ -176,7 +176,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
176 176
   end
177 177
 
178 178
   def save_emojis(parent)
179
-    do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
179
+    do_not_download = DomainBlock.reject_media?(parent.account.domain)
180 180
 
181 181
     return if do_not_download
182 182
 

+ 1
- 0
app/models/account.rb View File

@@ -98,6 +98,7 @@ class Account < ApplicationRecord
98 98
   scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
99 99
   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')) }
100 100
   scope :popular, -> { order('account_stats.followers_count desc') }
101
+  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
101 102
 
102 103
   delegate :email,
103 104
            :unconfirmed_email,

+ 1
- 0
app/models/custom_emoji.rb View File

@@ -39,6 +39,7 @@ class CustomEmoji < ApplicationRecord
39 39
   scope :local,      -> { where(domain: nil) }
40 40
   scope :remote,     -> { where.not(domain: nil) }
41 41
   scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
42
+  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
42 43
 
43 44
   remotable_attachment :image, LIMIT
44 45
 

+ 30
- 3
app/models/domain_block.rb View File

@@ -24,14 +24,41 @@ class DomainBlock < ApplicationRecord
24 24
 
25 25
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
26 26
 
27
-  def self.blocked?(domain)
28
-    where(domain: domain, severity: :suspend).exists?
27
+  class << self
28
+    def suspend?(domain)
29
+      !!rule_for(domain)&.suspend?
30
+    end
31
+
32
+    def silence?(domain)
33
+      !!rule_for(domain)&.silence?
34
+    end
35
+
36
+    def reject_media?(domain)
37
+      !!rule_for(domain)&.reject_media?
38
+    end
39
+
40
+    def reject_reports?(domain)
41
+      !!rule_for(domain)&.reject_reports?
42
+    end
43
+
44
+    alias blocked? suspend?
45
+
46
+    def rule_for(domain)
47
+      return if domain.blank?
48
+
49
+      uri      = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
50
+      segments = uri.normalized_host.split('.')
51
+      variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
52
+
53
+      where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first
54
+    end
29 55
   end
30 56
 
31 57
   def stricter_than?(other_block)
32
-    return true if suspend?
58
+    return true  if suspend?
33 59
     return false if other_block.suspend? && (silence? || noop?)
34 60
     return false if other_block.silence? && noop?
61
+
35 62
     (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
36 63
   end
37 64
 

+ 1
- 1
app/models/instance.rb View File

@@ -8,7 +8,7 @@ class Instance
8 8
   def initialize(resource)
9 9
     @domain         = resource.domain
10 10
     @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count
11
-    @domain_block   = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain)
11
+    @domain_block   = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
12 12
   end
13 13
 
14 14
   def cached_sample_accounts

+ 1
- 1
app/services/activitypub/process_account_service.rb View File

@@ -205,7 +205,7 @@ class ActivityPub::ProcessAccountService < BaseService
205 205
 
206 206
   def domain_block
207 207
     return @domain_block if defined?(@domain_block)
208
-    @domain_block = DomainBlock.find_by(domain: @domain)
208
+    @domain_block = DomainBlock.rule_for(@domain)
209 209
   end
210 210
 
211 211
   def key_changed?

+ 2
- 2
app/services/block_domain_service.rb View File

@@ -76,7 +76,7 @@ class BlockDomainService < BaseService
76 76
   end
77 77
 
78 78
   def blocked_domain_accounts
79
-    Account.where(domain: blocked_domain)
79
+    Account.by_domain_and_subdomains(blocked_domain)
80 80
   end
81 81
 
82 82
   def media_from_blocked_domain
@@ -84,6 +84,6 @@ class BlockDomainService < BaseService
84 84
   end
85 85
 
86 86
   def emojis_from_blocked_domains
87
-    CustomEmoji.where(domain: blocked_domain)
87
+    CustomEmoji.by_domain_and_subdomains(blocked_domain)
88 88
   end
89 89
 end

+ 1
- 1
app/services/resolve_account_service.rb View File

@@ -146,7 +146,7 @@ class ResolveAccountService < BaseService
146 146
 
147 147
   def domain_block
148 148
     return @domain_block if defined?(@domain_block)
149
-    @domain_block = DomainBlock.find_by(domain: @domain)
149
+    @domain_block = DomainBlock.rule_for(@domain)
150 150
   end
151 151
 
152 152
   def atom_url

+ 2
- 1
app/services/unblock_domain_service.rb View File

@@ -14,7 +14,8 @@ class UnblockDomainService < BaseService
14 14
   end
15 15
 
16 16
   def blocked_accounts
17
-    scope = Account.where(domain: domain_block.domain)
17
+    scope = Account.by_domain_and_subdomains(domain_block.domain)
18
+
18 19
     if domain_block.silence?
19 20
       scope.where(silenced_at: @domain_block.created_at)
20 21
     else

+ 2
- 2
app/services/update_remote_profile_service.rb View File

@@ -26,7 +26,7 @@ class UpdateRemoteProfileService < BaseService
26 26
     account.note         = remote_profile.note         || ''
27 27
     account.locked       = remote_profile.locked?
28 28
 
29
-    if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
29
+    if !account.suspended? && !DomainBlock.reject_media?(account.domain)
30 30
       if remote_profile.avatar.present?
31 31
         account.avatar_remote_url = remote_profile.avatar
32 32
       else
@@ -46,7 +46,7 @@ class UpdateRemoteProfileService < BaseService
46 46
   end
47 47
 
48 48
   def save_emojis
49
-    do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media?
49
+    do_not_download = DomainBlock.reject_media?(account.domain)
50 50
 
51 51
     return if do_not_download
52 52
 

+ 17
- 0
spec/models/account_spec.rb View File

@@ -687,6 +687,23 @@ RSpec.describe Account, type: :model do
687 687
       end
688 688
     end
689 689
 
690
+    describe 'by_domain_and_subdomains' do
691
+      it 'returns exact domain matches' do
692
+        account = Fabricate(:account, domain: 'example.com')
693
+        expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
694
+      end
695
+
696
+      it 'returns subdomains' do
697
+        account = Fabricate(:account, domain: 'foo.example.com')
698
+        expect(Account.by_domain_and_subdomains('example.com')).to eq [account]
699
+      end
700
+
701
+      it 'does not return partially matching domains' do
702
+        account = Fabricate(:account, domain: 'grexample.com')
703
+        expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account]
704
+      end
705
+    end
706
+
690 707
     describe 'expiring' do
691 708
       it 'returns remote accounts with followers whose subscription expiration date is past or not given' do
692 709
         local = Fabricate(:account, domain: nil)

+ 24
- 7
spec/models/domain_block_spec.rb View File

@@ -21,23 +21,40 @@ RSpec.describe DomainBlock, type: :model do
21 21
     end
22 22
   end
23 23
 
24
-  describe 'blocked?' do
24
+  describe '.blocked?' do
25 25
     it 'returns true if the domain is suspended' do
26
-      Fabricate(:domain_block, domain: 'domain', severity: :suspend)
27
-      expect(DomainBlock.blocked?('domain')).to eq true
26
+      Fabricate(:domain_block, domain: 'example.com', severity: :suspend)
27
+      expect(DomainBlock.blocked?('example.com')).to eq true
28 28
     end
29 29
 
30 30
     it 'returns false even if the domain is silenced' do
31
-      Fabricate(:domain_block, domain: 'domain', severity: :silence)
32
-      expect(DomainBlock.blocked?('domain')).to eq false
31
+      Fabricate(:domain_block, domain: 'example.com', severity: :silence)
32
+      expect(DomainBlock.blocked?('example.com')).to eq false
33 33
     end
34 34
 
35 35
     it 'returns false if the domain is not suspended nor silenced' do
36
-      expect(DomainBlock.blocked?('domain')).to eq false
36
+      expect(DomainBlock.blocked?('example.com')).to eq false
37 37
     end
38 38
   end
39 39
 
40
-  describe 'stricter_than?' do
40
+  describe '.rule_for' do
41
+    it 'returns rule matching a blocked domain' do
42
+      block = Fabricate(:domain_block, domain: 'example.com')
43
+      expect(DomainBlock.rule_for('example.com')).to eq block
44
+    end
45
+
46
+    it 'returns a rule matching a subdomain of a blocked domain' do
47
+      block = Fabricate(:domain_block, domain: 'example.com')
48
+      expect(DomainBlock.rule_for('sub.example.com')).to eq block
49
+    end
50
+
51
+    it 'returns a rule matching a blocked subdomain' do
52
+      block = Fabricate(:domain_block, domain: 'sub.example.com')
53
+      expect(DomainBlock.rule_for('sub.example.com')).to eq block
54
+    end
55
+  end
56
+
57
+  describe '#stricter_than?' do
41 58
     it 'returns true if the new block has suspend severity while the old has lower severity' do
42 59
       suspend = DomainBlock.new(domain: 'domain', severity: :suspend)
43 60
       silence = DomainBlock.new(domain: 'domain', severity: :silence)

Loading…
Cancel
Save