diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index b933b5dbb30e9b23bf139185e04a56afd85c44bf..1e4ca55b08d0b5d44699fc68a2be843e2128ee12 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -132,8 +132,10 @@ def create_spam_abuse_event(result) verdict: result } + base_class = Feature.enabled?(:rename_abuse_workers, user, type: :worker) ? AntiAbuse : Abuse + target.run_after_commit_or_now do - Abuse::SpamAbuseEventsWorker.perform_async(params) + base_class::SpamAbuseEventsWorker.perform_async(params) end end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index f69ee255e01ae2195a6006773da2c1ec7facab36..a8381589cc937ef82ff4120692b843588eaa6ba0 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -71,7 +71,8 @@ def get_spamcheck_verdict if result.evaluated? correlation_id = Labkit::Correlation::CorrelationId.current_id || '' - Abuse::TrustScoreWorker.perform_async(user.id, :spamcheck, result.score, correlation_id) + base_class = Feature.enabled?(:rename_abuse_workers, user, type: :worker) ? AntiAbuse : Abuse + base_class::TrustScoreWorker.perform_async(user.id, :spamcheck, result.score, correlation_id) end result.verdict diff --git a/app/workers/abuse/spam_abuse_events_worker.rb b/app/workers/abuse/spam_abuse_events_worker.rb index 65c669abfdba8db955f13325369ccdb36c6a7d63..6afda83d60467d58e09fe7377e14bb49b309a2c8 100644 --- a/app/workers/abuse/spam_abuse_events_worker.rb +++ b/app/workers/abuse/spam_abuse_events_worker.rb @@ -11,50 +11,7 @@ class SpamAbuseEventsWorker urgency :low def perform(params) - params = params.with_indifferent_access - - @user = User.find_by_id(params[:user_id]) - unless @user - logger.info(structured_payload(message: "User not found.", user_id: params[:user_id])) - return - end - - report_user(params) - end - - private - - attr_reader :user - - def report_user(params) - category = 'spam' - reporter = Users::Internal.security_bot - report_params = { user_id: params[:user_id], - reporter: reporter, - category: category, - message: 'User reported for abuse based on spam verdict' } - - abuse_report = AbuseReport.by_category(category).by_reporter_id(reporter.id).by_user_id(params[:user_id]).first - - abuse_report = AbuseReport.create!(report_params) if abuse_report.nil? - - create_abuse_event(abuse_report.id, params) - end - - # Associate the abuse report with an abuse event - def create_abuse_event(abuse_report_id, params) - AntiAbuse::Event.create!( - abuse_report_id: abuse_report_id, - category: :spam, - metadata: { noteable_type: params[:noteable_type], - title: params[:title], - description: params[:description], - source_ip: params[:source_ip], - user_agent: params[:user_agent], - verdict: params[:verdict] }, - source: :spamcheck, - user: user - ) + AntiAbuse::SpamAbuseEventsWorker.new.perform(params) end end end diff --git a/app/workers/abuse/trust_score_worker.rb b/app/workers/abuse/trust_score_worker.rb index 2bb09ddcbe9f3c0ed138df3550b6025c1f24ea29..d7b713964fb15c9108ff7e02221597df4f38469c 100644 --- a/app/workers/abuse/trust_score_worker.rb +++ b/app/workers/abuse/trust_score_worker.rb @@ -11,14 +11,7 @@ class TrustScoreWorker urgency :low def perform(user_id, source, score, correlation_id = '') - user = User.find_by_id(user_id) - unless user - logger.info(structured_payload(message: "User not found.", user_id: user_id)) - return - end - - AntiAbuse::TrustScore.create!(user: user, source: source, score: score.to_f, correlation_id_value: correlation_id) - AntiAbuse::TrustScoreCleanupWorker.perform_async(user.id, source) + AntiAbuse::TrustScoreWorker.new.perform(user_id, source, score, correlation_id) end end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 5e6c0803dc2a899ecd0e20643bf2df7667d1fac9..1709693ce367eb5c77a99124b519967ef2dfe486 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2415,6 +2415,24 @@ :weight: 1 :idempotent: true :tags: [] +- :name: anti_abuse_spam_abuse_events + :worker_name: AntiAbuse::SpamAbuseEventsWorker + :feature_category: :instance_resiliency + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: anti_abuse_trust_score + :worker_name: AntiAbuse::TrustScoreWorker + :feature_category: :instance_resiliency + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: anti_abuse_trust_score_cleanup :worker_name: AntiAbuse::TrustScoreCleanupWorker :feature_category: :instance_resiliency diff --git a/app/workers/anti_abuse/spam_abuse_events_worker.rb b/app/workers/anti_abuse/spam_abuse_events_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..53dfaae1b422676c1262d7e179983522187c76ec --- /dev/null +++ b/app/workers/anti_abuse/spam_abuse_events_worker.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module AntiAbuse + class SpamAbuseEventsWorker + include ApplicationWorker + + data_consistency :delayed + + idempotent! + feature_category :instance_resiliency + urgency :low + + def perform(params) + params = params.with_indifferent_access + + @user = User.find_by_id(params[:user_id]) + unless @user + logger.info(structured_payload(message: "User not found.", user_id: params[:user_id])) + return + end + + report_user(params) + end + + private + + attr_reader :user + + def report_user(params) + category = 'spam' + reporter = Users::Internal.security_bot + report_params = { user_id: params[:user_id], + reporter: reporter, + category: category, + message: 'User reported for abuse based on spam verdict' } + + abuse_report = AbuseReport.by_category(category).by_reporter_id(reporter.id).by_user_id(params[:user_id]).first + + abuse_report = AbuseReport.create!(report_params) if abuse_report.nil? + + create_abuse_event(abuse_report.id, params) + end + + # Associate the abuse report with an abuse event + def create_abuse_event(abuse_report_id, params) + AntiAbuse::Event.create!( + abuse_report_id: abuse_report_id, + category: :spam, + metadata: { noteable_type: params[:noteable_type], + title: params[:title], + description: params[:description], + source_ip: params[:source_ip], + user_agent: params[:user_agent], + verdict: params[:verdict] }, + source: :spamcheck, + user: user + ) + end + end +end diff --git a/app/workers/anti_abuse/trust_score_worker.rb b/app/workers/anti_abuse/trust_score_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..fc72b617e92032f89d850e96663573a56605d1fc --- /dev/null +++ b/app/workers/anti_abuse/trust_score_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module AntiAbuse + class TrustScoreWorker + include ApplicationWorker + + data_consistency :delayed + + idempotent! + feature_category :instance_resiliency + urgency :low + + def perform(user_id, source, score, correlation_id = '') + user = User.find_by_id(user_id) + unless user + logger.info(structured_payload(message: "User not found.", user_id: user_id)) + return + end + + AntiAbuse::TrustScore.create!(user: user, source: source, score: score.to_f, correlation_id_value: correlation_id) + AntiAbuse::TrustScoreCleanupWorker.perform_async(user.id, source) + end + end +end diff --git a/config/feature_flags/worker/rename_abuse_workers.yml b/config/feature_flags/worker/rename_abuse_workers.yml new file mode 100644 index 0000000000000000000000000000000000000000..85b33aca447efbb4a53377af542844915783eb28 --- /dev/null +++ b/config/feature_flags/worker/rename_abuse_workers.yml @@ -0,0 +1,9 @@ +--- +name: rename_abuse_workers +feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/850 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163792 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480273 +milestone: '17.4' +group: group::anti-abuse +type: worker +default_enabled: false diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index a62d4b789be2b93c04315c18af55fa9130e0ed5b..0ba0b02d36c76e36e6da2d22a822c4bc7e806c3c 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -43,6 +43,12 @@ - 1 - - analytics_usage_trends_counter_job - 1 +- - anti_abuse_new_abuse_report + - 1 +- - anti_abuse_spam_abuse_events + - 1 +- - anti_abuse_trust_score + - 1 - - anti_abuse_trust_score_cleanup - 1 - - app_sec_container_scanning_scan_image diff --git a/ee/app/models/ee/abuse_report.rb b/ee/app/models/ee/abuse_report.rb index 3e018c3ca6793afc3350ec95922c86b14b4ca3cc..0675e2cbe6660e84da71cd1a38f37d3d12c36b75 100644 --- a/ee/app/models/ee/abuse_report.rb +++ b/ee/app/models/ee/abuse_report.rb @@ -32,8 +32,10 @@ def reported_content private def run_abuse_report_worker + base_class = ::Feature.enabled?(:rename_abuse_workers, user, type: :worker) ? AntiAbuse : Abuse + run_after_commit_or_now do - Abuse::NewAbuseReportWorker.perform_async(id) + base_class::NewAbuseReportWorker.perform_async(id) end end end diff --git a/ee/app/services/arkose/record_user_data_service.rb b/ee/app/services/arkose/record_user_data_service.rb index 59c599325402dcc9d180c82244daabdfa916f5a0..43bd2fa2d2831bacaa77f665ab904ba0b98f436a 100644 --- a/ee/app/services/arkose/record_user_data_service.rb +++ b/ee/app/services/arkose/record_user_data_service.rb @@ -44,8 +44,10 @@ def custom_attributes end def store_risk_scores - Abuse::TrustScoreWorker.perform_async(user.id, :arkose_global_score, response.global_score.to_f) - Abuse::TrustScoreWorker.perform_async(user.id, :arkose_custom_score, response.custom_score.to_f) + base_class = Feature.enabled?(:rename_abuse_workers, user, type: :worker) ? AntiAbuse : Abuse + + base_class::TrustScoreWorker.perform_async(user.id, :arkose_global_score, response.global_score.to_f) + base_class::TrustScoreWorker.perform_async(user.id, :arkose_custom_score, response.custom_score.to_f) end def logger diff --git a/ee/app/services/phone_verification/users/record_user_data_service.rb b/ee/app/services/phone_verification/users/record_user_data_service.rb index ae202d6fc107a6db0c12bb22be854ba23d839a34..1d8d7deb292b68e7372685e21df06068539b3bc4 100644 --- a/ee/app/services/phone_verification/users/record_user_data_service.rb +++ b/ee/app/services/phone_verification/users/record_user_data_service.rb @@ -31,7 +31,8 @@ def error_related_to_high_risk_user end def store_risk_score(risk_score) - Abuse::TrustScoreWorker.perform_async(user.id, :telesign, risk_score.to_f) + base_class = Feature.enabled?(:rename_abuse_workers, user, type: :worker) ? AntiAbuse : Abuse + base_class::TrustScoreWorker.perform_async(user.id, :telesign, risk_score.to_f) end def assume_high_risk? diff --git a/ee/app/workers/abuse/new_abuse_report_worker.rb b/ee/app/workers/abuse/new_abuse_report_worker.rb index 007bfb1e585cf55a69b01f968814de6904304ff8..179ec2bba655139cc60b787802813e0adf0c92f0 100644 --- a/ee/app/workers/abuse/new_abuse_report_worker.rb +++ b/ee/app/workers/abuse/new_abuse_report_worker.rb @@ -11,49 +11,8 @@ class NewAbuseReportWorker idempotent! - attr_reader :user, :reporter, :abuse_report - def perform(abuse_report_id) - @abuse_report = AbuseReport.find_by_id(abuse_report_id) - return unless abuse_report&.category == 'spam' - - @reporter = abuse_report.reporter - @user = abuse_report.user - - return unless user && reporter - return unless reporter.gitlab_employee? - return unless bannable_user? - - ApplicationRecord.transaction do - Users::AutoBanService.new(user: user, reason: abuse_report.category).execute! - UserCustomAttribute.set_banned_by_abuse_report(abuse_report) - end - - log_event - end - - private - - def bannable_user? - return false unless user.active? && user.human? - return false if user.gitlab_employee? || user.account_age_in_days > 7 - return false if user.belongs_to_paid_namespace?(exclude_trials: true) || user_owns_populated_namespaces? - - true - end - - def user_owns_populated_namespaces? - user.owned_groups.find { |group| group.users_count > 5 } - end - - def log_event - Gitlab::AppLogger.info( - message: "User ban", - user_id: user.id, - username: user.username, - abuse_report_id: abuse_report.id, - reason: "Automatic ban triggered by abuse report for #{abuse_report.category}." - ) + AntiAbuse::NewAbuseReportWorker.new.perform(abuse_report_id) end end end diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index 28d6d825338f6e9a9b91963f06783f888b97668c..7fe38a1134a7cf7fbd3b5bb9a1c798cbdaa05d05 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -1029,6 +1029,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: anti_abuse_new_abuse_report + :worker_name: AntiAbuse::NewAbuseReportWorker + :feature_category: :instance_resiliency + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: app_sec_container_scanning_scan_image :worker_name: AppSec::ContainerScanning::ScanImageWorker :feature_category: :software_composition_analysis diff --git a/ee/app/workers/anti_abuse/new_abuse_report_worker.rb b/ee/app/workers/anti_abuse/new_abuse_report_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..0994084d6a9dee811ec8ecd42d8a3b28595a6f15 --- /dev/null +++ b/ee/app/workers/anti_abuse/new_abuse_report_worker.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module AntiAbuse + class NewAbuseReportWorker + include ApplicationWorker + + feature_category :instance_resiliency + + data_consistency :delayed + urgency :low + + idempotent! + + attr_reader :user, :reporter, :abuse_report + + def perform(abuse_report_id) + @abuse_report = AbuseReport.find_by_id(abuse_report_id) + return unless abuse_report&.category == 'spam' + + @reporter = abuse_report.reporter + @user = abuse_report.user + + return unless user && reporter + return unless reporter.gitlab_employee? + return unless bannable_user? + + ApplicationRecord.transaction do + Users::AutoBanService.new(user: user, reason: abuse_report.category).execute! + UserCustomAttribute.set_banned_by_abuse_report(abuse_report) + end + + log_event + end + + private + + def bannable_user? + return false unless user.active? && user.human? + return false if user.gitlab_employee? || user.account_age_in_days > 7 + return false if user.belongs_to_paid_namespace?(exclude_trials: true) || user_owns_populated_namespaces? + + true + end + + def user_owns_populated_namespaces? + user.owned_groups.find { |group| group.users_count > 5 } # rubocop: disable Gitlab/NoFindInWorkers -- not ActiveRecordFind + end + + def log_event + Gitlab::AppLogger.info( + message: "User ban", + user_id: user.id, + username: user.username, + abuse_report_id: abuse_report.id, + reason: "Automatic ban triggered by abuse report for #{abuse_report.category}." + ) + end + end +end diff --git a/ee/spec/models/abuse_report_spec.rb b/ee/spec/models/abuse_report_spec.rb index d529f6242b2e0990440cc045b78cf13784ae7811..32a393838b1ec612c0618fb81d746735f57e6f4d 100644 --- a/ee/spec/models/abuse_report_spec.rb +++ b/ee/spec/models/abuse_report_spec.rb @@ -4,9 +4,22 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do describe '.create' do - it 'calls the new abuse report worker' do - expect(Abuse::NewAbuseReportWorker).to receive(:perform_async) - create(:abuse_report) + context 'when the rename_abuse_workers feature is enabled' do + it 'calls the new abuse report worker' do + expect(AntiAbuse::NewAbuseReportWorker).to receive(:perform_async) + create(:abuse_report) + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'calls the new abuse report worker' do + expect(Abuse::NewAbuseReportWorker).to receive(:perform_async) + create(:abuse_report) + end end end diff --git a/ee/spec/services/arkose/record_user_data_service_spec.rb b/ee/spec/services/arkose/record_user_data_service_spec.rb index 0e2280b0079fa155f91cca37d29bcca47fb4a1f0..09f6fd98128d2b86907786ea307f4038f95769a9 100644 --- a/ee/spec/services/arkose/record_user_data_service_spec.rb +++ b/ee/spec/services/arkose/record_user_data_service_spec.rb @@ -30,15 +30,34 @@ expect(user.custom_attributes.find_by(key: 'arkose_custom_score').value).to eq('0') end - it 'executes abuse trust score workers' do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( - user.id, :arkose_global_score, 0.0 - ) - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( - user.id, :arkose_custom_score, 0.0 - ) + context 'when the rename_abuse_workers feature is enabled' do + it 'executes abuse trust score workers' do + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( + user.id, :arkose_global_score, 0.0 + ) + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( + user.id, :arkose_custom_score, 0.0 + ) - service.execute + service.execute + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'executes abuse trust score workers' do + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( + user.id, :arkose_global_score, 0.0 + ) + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.ordered.with( + user.id, :arkose_custom_score, 0.0 + ) + + service.execute + end end it 'logs user risk band assignment event' do diff --git a/ee/spec/services/phone_verification/users/record_user_data_service_spec.rb b/ee/spec/services/phone_verification/users/record_user_data_service_spec.rb index 52c5719bb864a75b00c5ea6b6ef782e7062e85e6..6ba4f1576fd1eb8885996f97bda407af2f8c9396 100644 --- a/ee/spec/services/phone_verification/users/record_user_data_service_spec.rb +++ b/ee/spec/services/phone_verification/users/record_user_data_service_spec.rb @@ -35,10 +35,25 @@ expect(phone_verification_record).not_to be_persisted end - it 'executes the abuse trust score worker' do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, low_risk_score.to_f) + context 'when the rename_abuse_workers feature is enabled' do + it 'executes the abuse trust score worker' do + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, + low_risk_score.to_f) - service.execute + service.execute + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'executes the abuse trust score worker' do + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, low_risk_score.to_f) + + service.execute + end + end end context 'when the risk score is 0' do @@ -48,10 +63,24 @@ expect { service.execute }.to change { phone_verification_record.risk_score }.from(0).to(1) end - it 'executes the abuse trust score worker with a risk score of 1.0' do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, 1.0) + context 'when the rename_abuse_workers feature is enabled' do + it 'executes the abuse trust score worker with a risk score of 1.0' do + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, 1.0) - service.execute + service.execute + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'executes the abuse trust score worker with a risk score of 1.0' do + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, 1.0) + + service.execute + end end end diff --git a/ee/spec/services/phone_verification/users/send_verification_code_service_spec.rb b/ee/spec/services/phone_verification/users/send_verification_code_service_spec.rb index f386bc1ab79f66290bffd529c92c0164888e5e6e..b752bbfbacc9c801abcff6d3ba54c82171a81cfc 100644 --- a/ee/spec/services/phone_verification/users/send_verification_code_service_spec.rb +++ b/ee/spec/services/phone_verification/users/send_verification_code_service_spec.rb @@ -422,12 +422,6 @@ expect(record.telesign_reference_xid).to eq(telesign_reference_xid) end - it 'executes the abuse trust score worker' do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :telesign, instance_of(Float)) - - service.execute - end - it 'updates sms_send_count and sms_sent_at', :freeze_time do service.execute record = user.phone_number_validation diff --git a/ee/spec/workers/anti_abuse/new_abuse_report_worker_spec.rb b/ee/spec/workers/anti_abuse/new_abuse_report_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..626c8d2b49f550117289cb236560a81fb7532308 --- /dev/null +++ b/ee/spec/workers/anti_abuse/new_abuse_report_worker_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AntiAbuse::NewAbuseReportWorker, :saas, feature_category: :instance_resiliency do + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:abuse_report) { create(:abuse_report, user: user, reporter: reporter) } + let_it_be(:job_args) { [abuse_report.id] } + + shared_examples 'bans user' do + it 'bans the user' do + expect(user).not_to be_banned + + allow(Gitlab::AppLogger).to receive(:info) + expect(Gitlab::AppLogger).to receive(:info).with( + message: "User ban", + user_id: user.id, + username: user.username, + abuse_report_id: abuse_report.id, + reason: "Automatic ban triggered by abuse report for #{abuse_report.category}." + ) + + worker.perform(*job_args) + + expect(user).to be_banned + expect( + user.custom_attributes.by_key(UserCustomAttribute::AUTO_BANNED_BY_ABUSE_REPORT_ID).first.value + ).to eq(abuse_report.id.to_s) + end + end + + shared_examples 'does not ban user' do + it 'does not ban the user' do + expect(user).not_to receive(:ban!) + expect(UserCustomAttribute).not_to receive(:upsert_custom_attributes) + worker.perform(*job_args) + + expect(user).not_to be_banned + end + end + + it_behaves_like 'an idempotent worker' do + subject(:worker) { described_class.new } + + context 'when reporter is a gitlab employee' do + before do + allow(reporter).to receive(:gitlab_employee?).and_return(true) + allow(user).to receive(:human?).and_return(true) + allow(user).to receive(:active?).and_return(true) + allow(AbuseReport).to receive(:find_by_id).and_return(abuse_report) + end + + context 'when the user is a member of a namespace with a paid plan trial subscription' do + let_it_be(:trial_group) do + create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: 1.day.from_now, reporters: user) + end + + it_behaves_like 'bans user' + end + + context 'when the user is a member of a namespace with a paid plan subscription' do + let_it_be(:paid_group) { create(:group_with_plan, plan: :ultimate_plan, reporters: user) } + + it_behaves_like 'does not ban user' + end + + context 'when the user is on a free plan' do + context 'when the user is human' do + it_behaves_like 'bans user' + end + + context 'when the user is not human' do + before do + allow(user).to receive(:human?).and_return(false) + end + + it_behaves_like 'does not ban user' + end + end + + context 'when the user is not active' do + before do + allow(user).to receive(:active?).and_return(false) + end + + it_behaves_like 'does not ban user' + end + + context 'when the user is not found' do + before do + allow(abuse_report).to receive(:user).and_return(nil) + end + + it 'does not start a transaction' do + expect(described_class).not_to receive(:bannable_user?) + + worker.perform(*job_args) + end + end + + context 'when the user is a gitlab employee' do + before do + allow(user).to receive(:gitlab_employee?).and_return(true) + end + + it_behaves_like 'does not ban user' + end + + context 'when the user age is greater than 7 days' do + before do + user.update_attribute(:created_at, 8.days.ago) + end + + it_behaves_like 'does not ban user' + end + + context 'when user owns namespaces with more than five members' do + let_it_be(:group) { create(:group, owners: user, guests: create_list(:user, 5)) } + + it_behaves_like 'does not ban user' + end + end + + context 'when reporter is not a gitlab employee' do + it_behaves_like 'does not ban user' + end + + context 'when the abuse report is not found' do + before do + allow(AbuseReport).to receive(:find_by_id).and_return(nil) + end + + it 'returns early' do + expect(abuse_report).not_to receive(:reporter) + + worker.perform(*job_args) + end + end + + context 'when there is an error executing ban' do + before do + allow(user).to receive(:ban!).and_raise(StateMachines::InvalidTransition) + end + + it 'does not emit an application log' do + expect(Gitlab::AppLogger).not_to receive(:info) + + worker.perform(*job_args) + end + end + end +end diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb index 315d4714516e03934b6c7660df693a2442f2c8cc..151b3ed43fa93dd2abfe3179e7fcb431bd69b37f 100644 --- a/spec/services/spam/spam_action_service_spec.rb +++ b/spec/services/spam/spam_action_service_spec.rb @@ -98,8 +98,45 @@ } end - it do - expect(::Abuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params) + context 'when the rename_abuse_workers feature is enabled' do + it 'executes the ::AntiAbuse::SpamAbuseEventsWorker' do + expect(::AntiAbuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params) + + subject + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'executes the ::Abuse::SpamAbuseEventsWorker' do + expect(::Abuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params) + + subject + end + end + end + + shared_examples 'does not execute the SpamAbuseEventsWorker' do + specify do + expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async) + expect(::AntiAbuse::SpamAbuseEventsWorker).not_to receive(:perform_async) + + subject + end + end + + shared_examples 'allows the spammable' do + it 'does not create a spam log' do + expect { subject }.not_to change(SpamLog, :count) + end + + it_behaves_like 'does not execute the SpamAbuseEventsWorker' + + it 'clears spam flags' do + expect(target).to receive(:clear_spam_flags!) subject end @@ -182,11 +219,7 @@ expect { subject }.not_to change(SpamLog, :count) end - it 'does not call SpamAbuseEventsWorker' do - expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async) - - subject - end + it_behaves_like 'does not execute the SpamAbuseEventsWorker' end context 'when spammable attributes have changed' do @@ -317,12 +350,7 @@ end it_behaves_like 'creates a spam log', target_type - - it 'does not call SpamAbuseEventsWorker' do - expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async) - - subject - end + it_behaves_like 'does not execute the SpamAbuseEventsWorker' it 'does not mark as spam' do response = subject @@ -344,21 +372,7 @@ allow(fake_verdict_service).to receive(:execute).and_return(ALLOW) end - it 'does not create a spam log' do - expect { subject }.not_to change(SpamLog, :count) - end - - it 'does not call SpamAbuseEventsWorker' do - expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async) - - subject - end - - it 'clears spam flags' do - expect(target).to receive(:clear_spam_flags!) - - subject - end + it_behaves_like 'allows the spammable' end context 'when spam verdict service returns noop' do @@ -366,21 +380,7 @@ allow(fake_verdict_service).to receive(:execute).and_return(NOOP) end - it 'does not create a spam log' do - expect { subject }.not_to change(SpamLog, :count) - end - - it 'does not call SpamAbuseEventsWorker' do - expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async) - - subject - end - - it 'clears spam flags' do - expect(target).to receive(:clear_spam_flags!) - - subject - end + it_behaves_like 'allows the spammable' end context 'with spam verdict service options' do diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 8669bca90bd8b462686fcbb752b4ead67f461344..f07b3d0d27f1caac712b1ccf562e39ca2ce51f6c 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -275,6 +275,7 @@ it 'returns the verdict' do expect(Abuse::TrustScoreWorker).not_to receive(:perform_async) + expect(AntiAbuse::TrustScoreWorker).not_to receive(:perform_async) is_expected.to eq(NOOP) end end @@ -284,9 +285,22 @@ let(:verdict_value) { ::Spamcheck::SpamVerdict::Verdict::ALLOW } context 'the result was evaluated' do - it 'returns the verdict and updates the spam score' do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') - is_expected.to eq(ALLOW) + context 'when the rename_abuse_workers feature is enabled' do + it 'returns the verdict and updates the spam score' do + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') + is_expected.to eq(ALLOW) + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it 'returns the verdict and updates the spam score' do + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') + is_expected.to eq(ALLOW) + end end end @@ -295,6 +309,7 @@ it 'returns the verdict and does not update the spam score' do expect(Abuse::TrustScoreWorker).not_to receive(:perform_async) + expect(AntiAbuse::TrustScoreWorker).not_to receive(:perform_async) expect(subject).to eq(ALLOW) end end @@ -315,9 +330,22 @@ end with_them do - it "returns expected spam constant and updates the spam score" do - expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') - is_expected.to eq(expected) + context 'when the rename_abuse_workers feature is enabled' do + it "returns expected spam constant and updates the spam score" do + expect(AntiAbuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') + is_expected.to eq(expected) + end + end + + context 'when the rename_abuse_workers feature is not enabled' do + before do + stub_feature_flags(rename_abuse_workers: false) + end + + it "returns expected spam constant and updates the spam score" do + expect(Abuse::TrustScoreWorker).to receive(:perform_async).once.with(user.id, :spamcheck, instance_of(Float), 'cid') + is_expected.to eq(expected) + end end end end diff --git a/spec/workers/anti_abuse/spam_abuse_events_worker_spec.rb b/spec/workers/anti_abuse/spam_abuse_events_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..adaa1b88656c9f49d23401ca984cb1d55ee2c414 --- /dev/null +++ b/spec/workers/anti_abuse/spam_abuse_events_worker_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AntiAbuse::SpamAbuseEventsWorker, :clean_gitlab_redis_shared_state, feature_category: :instance_resiliency do + let(:worker) { described_class.new } + let_it_be(:user) { create(:user) } + + let(:params) do + { + user_id: user.id, + title: 'Test title', + description: 'Test description', + source_ip: '1.2.3.4', + user_agent: 'fake-user-agent', + noteable_type: 'Issue', + verdict: 'BLOCK_USER' + } + end + + shared_examples 'creates an abuse event with the correct data' do + it do + expect { worker.perform(params) }.to change { AntiAbuse::Event.count }.from(0).to(1) + expect(AntiAbuse::Event.last.attributes).to include({ + abuse_report_id: report_id, + category: "spam", + metadata: params.except(:user_id), + source: "spamcheck", + user_id: params[:user_id] + }.deep_stringify_keys) + end + end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { [params] } + end + + context "when the user does not exist" do + let(:log_payload) { { 'message' => 'User not found.', 'user_id' => user.id } } + + before do + allow(User).to receive(:find_by_id).with(user.id).and_return(nil) + end + + it 'logs an error' do + expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload)) + + expect { worker.perform(params) }.not_to raise_exception + end + + it 'does not report the user' do + expect(described_class).not_to receive(:report_user).with(user.id) + + worker.perform(params) + end + end + + context "when the user exists" do + context 'and there is an existing abuse report' do + let_it_be(:abuse_report) do + create(:abuse_report, user: user, reporter: Users::Internal.security_bot, message: 'Test report') + end + + it_behaves_like 'creates an abuse event with the correct data' do + let(:report_id) { abuse_report.id } + end + end + + context 'and there is no existing abuse report' do + it 'creates an abuse report with the correct data' do + expect { worker.perform(params) }.to change { AbuseReport.count }.from(0).to(1) + expect(AbuseReport.last.attributes).to include({ + reporter_id: Users::Internal.security_bot.id, + user_id: user.id, + category: "spam", + message: "User reported for abuse based on spam verdict" + }.stringify_keys) + end + + it_behaves_like 'creates an abuse event with the correct data' do + let(:report_id) { AbuseReport.last.attributes["id"] } + end + end + end +end diff --git a/spec/workers/anti_abuse/trust_score_worker_spec.rb b/spec/workers/anti_abuse/trust_score_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d0cb6618d21fb6d1f4f3b4dd7017ad77bd1a914f --- /dev/null +++ b/spec/workers/anti_abuse/trust_score_worker_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AntiAbuse::TrustScoreWorker, :clean_gitlab_redis_shared_state, feature_category: :instance_resiliency do + let(:worker) { described_class.new } + let_it_be(:user) { create(:user) } + + subject(:perform) { worker.perform(user.id, :telesign, 0.85, 'foo') } + + it_behaves_like 'an idempotent worker' do + let(:job_args) { [user.id, :telesign, 0.5] } + end + + context "when the user does not exist" do + let(:log_payload) { { 'message' => 'User not found.', 'user_id' => user.id } } + + before do + allow(User).to receive(:find_by_id).with(user.id).and_return(nil) + end + + it 'logs an error' do + expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload)) + + expect { perform }.not_to raise_exception + end + + it 'does not attempt to create the trust score' do + expect(AntiAbuse::TrustScore).not_to receive(:create!) + + perform + end + end + + context "when the user exists" do + it 'creates an abuse trust score with the correct data' do + expect { perform }.to change { AntiAbuse::TrustScore.count }.from(0).to(1) + expect(AntiAbuse::TrustScore.last.attributes).to include({ + user_id: user.id, + source: "telesign", + score: 0.85, + correlation_id_value: 'foo' + }.stringify_keys) + end + end +end