Commit 061dbe03 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'caw-refactor-spam-services-apis' into 'master'

Refactoring and simplification of spam-related services [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58603
parents 58afb092 6a9f0c27
...@@ -136,7 +136,7 @@ module Boards ...@@ -136,7 +136,7 @@ module Boards
def issue_params def issue_params
params.require(:issue) params.require(:issue)
.permit(:title, :milestone_id, :project_id) .permit(:title, :milestone_id, :project_id)
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request) .merge(board_id: params[:board_id], list_id: params[:list_id])
end end
def serializer def serializer
......
...@@ -47,31 +47,20 @@ module SpammableActions ...@@ -47,31 +47,20 @@ module SpammableActions
end end
end end
def spammable_params # TODO: This method is currently only needed for issue create and update. It can be removed when:
# NOTE: For the legacy reCAPTCHA implementation based on the HTML/HAML form, the
# 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
# recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
#
# It is used in the `Recaptcha::Verify#verify_recaptcha` to extract the value from `params`,
# if the `response` option is not passed explicitly.
#
# Instead of relying on this behavior, we are extracting and passing it explicitly. This will
# make it consistent with the newer, modern reCAPTCHA verification process as it will be
# implemented via the GraphQL API and in Vue components via the native reCAPTCHA Javascript API,
# which requires that the recaptcha response param be obtained and passed explicitly.
# #
# It can also be expanded to multiple fields when we move to future alternative captcha # 1. Issue create is is converted to a client/JS based approach instead of the legacy HAML
# implementations such as FriendlyCaptcha. See https://gitlab.com/gitlab-org/gitlab/-/issues/273480 # `_recaptcha_form.html.haml` which is rendered via the `projects/issues/verify` template.
# In this case, which is based on the legacy reCAPTCHA implementation using the HTML/HAML form,
# After this newer GraphQL/JS API process is fully supported by the backend, we can remove the # the 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
# check for the 'g-recaptcha-response' field and other HTML/HAML form-specific support. # recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
captcha_response = params['g-recaptcha-response'] || params[:captcha_response] # 2. Issue update is converted to use the headers-based approach, which will require adding
# support to captcha_modal_axios_interceptor.js like we have already added to
{ # apollo_captcha_link.js.
request: request, # In this case, the `captcha_response` field name comes from our captcha_modal_axios_interceptor.js.
spam_log_id: params[:spam_log_id], def extract_legacy_spam_params_to_headers
captcha_response: captcha_response request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] || params[:captcha_response]
} request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id]
end end
def spammable def spammable
......
...@@ -129,12 +129,14 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -129,12 +129,14 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create def create
create_params = issue_params.merge(spammable_params).merge( extract_legacy_spam_params_to_headers
create_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve] discussion_to_resolve: params[:discussion_to_resolve]
) )
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params) spam_params = ::Spam::SpamParams.new_from_request(request: request)
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params, spam_params: spam_params)
@issue = service.execute @issue = service.execute
create_vulnerability_issue_feedback(issue) create_vulnerability_issue_feedback(issue)
...@@ -334,8 +336,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -334,8 +336,9 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def update_service def update_service
update_params = issue_params.merge(spammable_params) extract_legacy_spam_params_to_headers
::Issues::UpdateService.new(project: project, current_user: current_user, params: update_params) spam_params = ::Spam::SpamParams.new_from_request(request: request)
::Issues::UpdateService.new(project: project, current_user: current_user, params: issue_params, spam_params: spam_params)
end end
def finder_type def finder_type
......
...@@ -16,25 +16,6 @@ module Mutations ...@@ -16,25 +16,6 @@ module Mutations
private private
# additional_spam_params -> hash
#
# Used from a spammable mutation's #resolve method to generate
# the required additional spam/CAPTCHA params which must be merged into the params
# passed to the constructor of a service, where they can then be used in the service
# to perform spam checking via SpamActionService.
#
# Also accesses the #context of the mutation's Resolver superclass to obtain the request.
#
# Example:
#
# existing_args.merge!(additional_spam_params)
def additional_spam_params
{
api: true,
request: context[:request]
}
end
def spam_action_response(object) def spam_action_response(object)
fields = spam_action_response_fields(object) fields = spam_action_response_fields(object)
......
...@@ -73,7 +73,8 @@ module Mutations ...@@ -73,7 +73,8 @@ module Mutations
project = authorized_find!(project_path) project = authorized_find!(project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id)) params = build_create_issue_params(attributes.merge(author_id: current_user.id))
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
if issue.spam? if issue.spam?
issue.errors.add(:base, 'Spam detected.') issue.errors.add(:base, 'Spam detected.')
......
...@@ -13,8 +13,11 @@ module Mutations ...@@ -13,8 +13,11 @@ module Mutations
def resolve(project_path:, iid:, confidential:) def resolve(project_path:, iid:, confidential:)
issue = authorized_find!(project_path: project_path, iid: iid) issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project project = issue.project
# Changing confidentiality affects spam checking rules, therefore we need to provide
# spam_params so a check can be performed.
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential }) ::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params)
.execute(issue) .execute(issue)
{ {
......
...@@ -31,7 +31,8 @@ module Mutations ...@@ -31,7 +31,8 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid) issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project project = issue.project
::Issues::UpdateService.new(project: project, current_user: current_user, params: args).execute(issue) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue)
{ {
issue: issue, issue: issue,
......
...@@ -49,7 +49,9 @@ module Mutations ...@@ -49,7 +49,9 @@ module Mutations
process_args_for_params!(args) process_args_for_params!(args)
service_response = ::Snippets::CreateService.new(project: project, current_user: current_user, params: args).execute spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
service = ::Snippets::CreateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params)
service_response = service.execute
# Only when the user is not an api user and the operation was successful # Only when the user is not an api user and the operation was successful
if !api_user? && service_response.success? if !api_user? && service_response.success?
...@@ -81,12 +83,6 @@ module Mutations ...@@ -81,12 +83,6 @@ module Mutations
# it's the expected key param # it's the expected key param
args[:files] = args.delete(:uploaded_files) args[:files] = args.delete(:uploaded_files)
if Feature.enabled?(:snippet_spam)
args.merge!(additional_spam_params)
else
args[:disable_spam_action_service] = true
end
# Return nil to make it explicit that this method is mutating the args parameter, and that # Return nil to make it explicit that this method is mutating the args parameter, and that
# the return value is not relevant and is not to be used. # the return value is not relevant and is not to be used.
nil nil
......
...@@ -34,7 +34,9 @@ module Mutations ...@@ -34,7 +34,9 @@ module Mutations
process_args_for_params!(args) process_args_for_params!(args)
service_response = ::Snippets::UpdateService.new(project: snippet.project, current_user: current_user, params: args).execute(snippet) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
service = ::Snippets::UpdateService.new(project: snippet.project, current_user: current_user, params: args, spam_params: spam_params)
service_response = service.execute(snippet)
# TODO: DRY this up - From here down, this is all duplicated with Mutations::Snippets::Create#resolve, except for # TODO: DRY this up - From here down, this is all duplicated with Mutations::Snippets::Create#resolve, except for
# `snippet.reset`, which is required in order to return the object in its non-dirty, unmodified, database state # `snippet.reset`, which is required in order to return the object in its non-dirty, unmodified, database state
...@@ -62,12 +64,6 @@ module Mutations ...@@ -62,12 +64,6 @@ module Mutations
def process_args_for_params!(args) def process_args_for_params!(args)
convert_blob_actions_to_snippet_actions!(args) convert_blob_actions_to_snippet_actions!(args)
if Feature.enabled?(:snippet_spam)
args.merge!(additional_spam_params)
else
args[:disable_spam_action_service] = true
end
# Return nil to make it explicit that this method is mutating the args parameter, and that # Return nil to make it explicit that this method is mutating the args parameter, and that
# the return value is not relevant and is not to be used. # the return value is not relevant and is not to be used.
nil nil
......
...@@ -30,7 +30,9 @@ module Boards ...@@ -30,7 +30,9 @@ module Boards
end end
def create_issue(params) def create_issue(params)
::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute # NOTE: We are intentionally not doing a spam/CAPTCHA check for issues created via boards.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/29400#note_598479184 for more context.
::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: nil).execute
end end
end end
end end
......
...@@ -7,20 +7,27 @@ module Captcha ...@@ -7,20 +7,27 @@ module Captcha
class CaptchaVerificationService class CaptchaVerificationService
include Recaptcha::Verify include Recaptcha::Verify
# Currently the only value that is used out of the request by the reCAPTCHA library
# is 'remote_ip'. Therefore, we just create a struct to avoid passing the full request
# object through all the service layer objects, and instead just rely on passing only
# the required remote_ip value. This eliminates the need to couple the service layer
# to the HTTP request (for the purpose of this service, at least).
RequestStruct = Struct.new(:remote_ip)
def initialize(spam_params:)
@spam_params = spam_params
end
## ##
# Performs verification of a captcha response. # Performs verification of a captcha response.
# #
# 'captcha_response' parameter is the response from the user solving a client-side captcha.
#
# 'request' parameter is the request which submitted the captcha.
#
# NOTE: Currently only supports reCAPTCHA, and is not yet used in all places of the app in which # NOTE: Currently only supports reCAPTCHA, and is not yet used in all places of the app in which
# captchas are verified, but these can be addressed in future MRs. See: # captchas are verified, but these can be addressed in future MRs. See:
# https://gitlab.com/gitlab-org/gitlab/-/issues/273480 # https://gitlab.com/gitlab-org/gitlab/-/issues/273480
def execute(captcha_response: nil, request:) def execute
return false unless captcha_response return false unless spam_params.captcha_response
@request = request @request = RequestStruct.new(spam_params.ip_address)
Gitlab::Recaptcha.load_configurations! Gitlab::Recaptcha.load_configurations!
...@@ -31,11 +38,13 @@ module Captcha ...@@ -31,11 +38,13 @@ module Captcha
# 2. We want control over the wording and i18n of the message # 2. We want control over the wording and i18n of the message
# 3. We want a consistent interface and behavior when adding support for other captcha # 3. We want a consistent interface and behavior when adding support for other captcha
# libraries which may not support automatically adding errors to the model. # libraries which may not support automatically adding errors to the model.
verify_recaptcha(response: captcha_response) verify_recaptcha(response: spam_params.captcha_response)
end end
private private
attr_reader :spam_params
# The recaptcha library's Recaptcha::Verify#verify_recaptcha method requires that # The recaptcha library's Recaptcha::Verify#verify_recaptcha method requires that
# 'request' be a readable attribute - it doesn't support passing it as an options argument. # 'request' be a readable attribute - it doesn't support passing it as an options argument.
attr_reader :request attr_reader :request
......
...@@ -21,7 +21,8 @@ module IncidentManagement ...@@ -21,7 +21,8 @@ module IncidentManagement
title: title, title: title,
description: description, description: description,
issue_type: ISSUE_TYPE issue_type: ISSUE_TYPE
} },
spam_params: nil
).execute ).execute
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid? return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
......
...@@ -68,7 +68,10 @@ module Issuable ...@@ -68,7 +68,10 @@ module Issuable
end end
def create_issuable(attributes) def create_issuable(attributes)
create_issuable_class.new(project: @project, current_user: @user, params: attributes).execute # NOTE: CSV imports are performed by workers, so we do not have a request context in order
# to create a SpamParams object to pass to the issuable create service.
spam_params = nil
create_issuable_class.new(project: @project, current_user: @user, params: attributes, spam_params: spam_params).execute
end end
def email_results_to_user def email_results_to_user
......
...@@ -55,9 +55,13 @@ module Issues ...@@ -55,9 +55,13 @@ module Issues
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params) new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
# Skip creation of system notes for existing attributes of the issue. The system notes of the old # Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes. # issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(project: target_project, current_user: current_user, params: new_params).execute(skip_system_notes: true) CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
end end
def queue_copy_designs def queue_copy_designs
......
...@@ -4,10 +4,21 @@ module Issues ...@@ -4,10 +4,21 @@ module Issues
class CreateService < Issues::BaseService class CreateService < Issues::BaseService
include ResolveDiscussions include ResolveDiscussions
def execute(skip_system_notes: false) # NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
@request = params.delete(:request) # spam_checking is likely to be necessary. However, if there is not a request available in scope
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request) # in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(project:, current_user: nil, params: {}, spam_params:)
# Temporary check to ensure we are no longer passing request in params now that we have
# introduced spam_params. Raise an exception if it is present.
# Remove after https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58603 is complete.
raise if params[:request]
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(skip_system_notes: false)
@issue = BuildService.new(project: project, current_user: current_user, params: params).execute @issue = BuildService.new(project: project, current_user: current_user, params: params).execute
filter_resolve_discussion_params filter_resolve_discussion_params
...@@ -18,10 +29,10 @@ module Issues ...@@ -18,10 +29,10 @@ module Issues
def before_create(issue) def before_create(issue)
Spam::SpamActionService.new( Spam::SpamActionService.new(
spammable: issue, spammable: issue,
request: request, spam_params: spam_params,
user: current_user, user: current_user,
action: :create action: :create
).execute(spam_params: spam_params) ).execute
# current_user (defined in BaseService) is not available within run_after_commit block # current_user (defined in BaseService) is not available within run_after_commit block
user = current_user user = current_user
...@@ -64,10 +75,10 @@ module Issues ...@@ -64,10 +75,10 @@ module Issues
private private
attr_reader :request, :spam_params attr_reader :spam_params
def user_agent_detail_service def user_agent_detail_service
UserAgentDetailService.new(@issue, request) UserAgentDetailService.new(spammable: @issue, spam_params: spam_params)
end end
end end
end end
......
...@@ -28,6 +28,7 @@ module Issues ...@@ -28,6 +28,7 @@ module Issues
def relate_two_issues(duplicate_issue, canonical_issue) def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue } params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end end
end end
......
...@@ -58,10 +58,13 @@ module Issues ...@@ -58,10 +58,13 @@ module Issues
} }
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params) new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
# Skip creation of system notes for existing attributes of the issue. The system notes of the old # Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes. # issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(project: @target_project, current_user: @current_user, params: new_params).execute(skip_system_notes: true) CreateService.new(project: @target_project, current_user: @current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
end end
def queue_copy_designs def queue_copy_designs
......
...@@ -4,12 +4,17 @@ module Issues ...@@ -4,12 +4,17 @@ module Issues
class UpdateService < Issues::BaseService class UpdateService < Issues::BaseService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
# NOTE: For Issues::UpdateService, we default the spam_params to nil, because spam_checking is not
# necessary in many cases, and we don't want to require every caller to explicitly pass it as nil
# to disable spam checking.
def initialize(project:, current_user: nil, params: {}, spam_params: nil)
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(issue) def execute(issue)
handle_move_between_ids(issue) handle_move_between_ids(issue)
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
change_issue_duplicate(issue) change_issue_duplicate(issue)
move_issue_to_new_project(issue) || clone_issue(issue) || update_task_event(issue) || update(issue) move_issue_to_new_project(issue) || clone_issue(issue) || update_task_event(issue) || update(issue)
end end
...@@ -25,10 +30,10 @@ module Issues ...@@ -25,10 +30,10 @@ module Issues
Spam::SpamActionService.new( Spam::SpamActionService.new(
spammable: issue, spammable: issue,
request: request, spam_params: spam_params,
user: current_user, user: current_user,
action: :update action: :update
).execute(spam_params: spam_params) ).execute
end end
def handle_changes(issue, options) def handle_changes(issue, options)
...@@ -127,7 +132,7 @@ module Issues ...@@ -127,7 +132,7 @@ module Issues
private private
attr_reader :request, :spam_params attr_reader :spam_params
def clone_issue(issue) def clone_issue(issue)
target_project = params.delete(:target_clone_project) target_project = params.delete(:target_clone_project)
......
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
module Snippets module Snippets
class CreateService < Snippets::BaseService class CreateService < Snippets::BaseService
def execute # NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed. # spam_checking is likely to be necessary.
disable_spam_action_service = params.delete(:disable_spam_action_service) == true def initialize(project:, current_user: nil, params: {}, spam_params:)
@request = params.delete(:request) super(project: project, current_user: current_user, params: params)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request) @spam_params = spam_params
end
def execute
@snippet = build_from_params @snippet = build_from_params
return invalid_params_error(@snippet) unless valid_params? return invalid_params_error(@snippet) unless valid_params?
...@@ -18,17 +20,17 @@ module Snippets ...@@ -18,17 +20,17 @@ module Snippets
@snippet.author = current_user @snippet.author = current_user
unless disable_spam_action_service if Feature.enabled?(:snippet_spam)
Spam::SpamActionService.new( Spam::SpamActionService.new(
spammable: @snippet, spammable: @snippet,
request: request, spam_params: spam_params,
user: current_user, user: current_user,
action: :create action: :create
).execute(spam_params: spam_params) ).execute
end end
if save_and_commit if save_and_commit
UserAgentDetailService.new(@snippet, request).create UserAgentDetailService.new(spammable: @snippet, spam_params: spam_params).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create) Gitlab::UsageDataCounters::SnippetCounter.count(:create)
move_temporary_files move_temporary_files
...@@ -41,7 +43,7 @@ module Snippets ...@@ -41,7 +43,7 @@ module Snippets
private private
attr_reader :snippet, :request, :spam_params attr_reader :snippet, :spam_params
def build_from_params def build_from_params
if project if project
......
...@@ -6,12 +6,15 @@ module Snippets ...@@ -6,12 +6,15 @@ module Snippets
UpdateError = Class.new(StandardError) UpdateError = Class.new(StandardError)
def execute(snippet) # NOTE: For Snippets::UpdateService, we default the spam_params to nil, because spam_checking is not
# NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed. # necessary in many cases, and we don't want every caller to have to explicitly pass it as nil
disable_spam_action_service = params.delete(:disable_spam_action_service) == true # to disable spam checking.
@request = params.delete(:request) def initialize(project:, current_user: nil, params: {}, spam_params: nil)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request) super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(snippet)
return invalid_params_error(snippet) unless valid_params? return invalid_params_error(snippet) unless valid_params?
if visibility_changed?(snippet) && !visibility_allowed?(visibility_level) if visibility_changed?(snippet) && !visibility_allowed?(visibility_level)
...@@ -20,13 +23,13 @@ module Snippets ...@@ -20,13 +23,13 @@ module Snippets
update_snippet_attributes(snippet) update_snippet_attributes(snippet)
unless disable_spam_action_service if Feature.enabled?(:snippet_spam)
Spam::SpamActionService.new( Spam::SpamActionService.new(
spammable: snippet, spammable: snippet,
request: request, spam_params: spam_params,
user: current_user, user: current_user,
action: :update action: :update
).execute(spam_params: spam_params) ).execute
end end
if save_and_commit(snippet) if save_and_commit(snippet)
...@@ -40,7 +43,7 @@ module Snippets ...@@ -40,7 +43,7 @@ module Snippets
private private
attr_reader :request, :spam_params attr_reader :spam_params
def visibility_changed?(snippet) def visibility_changed?(snippet)
visibility_level && visibility_level.to_i != snippet.visibility_level visibility_level && visibility_level.to_i != snippet.visibility_level
......
...@@ -20,6 +20,7 @@ module Spam ...@@ -20,6 +20,7 @@ module Spam
created_at: DateTime.current, created_at: DateTime.current,
author: owner_name, author: owner_name,
author_email: owner_email, author_email: owner_email,
# NOTE: The akismet_client needs the option to be named `:referrer`, not `:referer`
referrer: options[:referer] referrer: options[:referer]
} }
......
...@@ -4,67 +4,22 @@ module Spam ...@@ -4,67 +4,22 @@ module Spam
class SpamActionService class SpamActionService
include SpamConstants include SpamConstants
## def initialize(spammable:, spam_params:, user:, action:)
# Utility method to filter SpamParams from parameters, which will later be passed to #execute
# after the spammable is created/updated based on the remaining parameters.
#
# Takes a hash of parameters from an incoming request to modify a model (via a controller,
# service, or GraphQL mutation). The parameters will either be camelCase (if they are
# received directly via controller params) or underscore_case (if they have come from
# a GraphQL mutation which has converted them to underscore), or in the
# headers when using the header based flow.
#
# Deletes the parameters which are related to spam and captcha processing, and returns
# them in a SpamParams parameters object. See:
# https://refactoring.com/catalog/introduceParameterObject.html
def self.filter_spam_params!(params, request)
# NOTE: The 'captcha_response' field can be expanded to multiple fields when we move to future
# alternative captcha implementations such as FriendlyCaptcha. See
# https://gitlab.com/gitlab-org/gitlab/-/issues/273480
headers = request&.headers || {}
api = params.delete(:api)
captcha_response = read_parameter(:captcha_response, params, headers)
spam_log_id = read_parameter(:spam_log_id, params, headers)&.to_i
SpamParams.new(api: api, captcha_response: captcha_response, spam_log_id: spam_log_id)
end
def self.read_parameter(name, params, headers)
[
params.delete(name),
params.delete(name.to_s.camelize(:lower).to_sym),
headers["X-GitLab-#{name.to_s.titlecase(keep_id_suffix: true).tr(' ', '-')}"]
].compact.first
end
attr_accessor :target, :request, :options
attr_reader :spam_log
def initialize(spammable:, request:, user:, action:)
@target = spammable @target = spammable
@request = request @spam_params = spam_params
@user = user @user = user
@action = action @action = action
@options = {}
end end
# rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/AbcSize
def execute(spam_params:) def execute
if request # If spam_params is passed as `nil`, no check will be performed. This is the easiest way to allow
options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s # composed services which may not need to do spam checking to "opt out". For example, when
options[:user_agent] = request.env['HTTP_USER_AGENT'] # MoveService is calling CreateService, spam checking is not necessary, as no new content is
options[:referer] = request.env['HTTP_REFERER'] # being created.
else return ServiceResponse.success(message: 'Skipped spam check because spam_params was not present') unless spam_params
# TODO: This code is never used, because we do not perform a verification if there is not a
# request. Why? Should it be deleted? Or should we check even if there is no request?
options[:ip_address] = target.ip_address
options[:user_agent] = target.user_agent
end
recaptcha_verified = Captcha::CaptchaVerificationService.new.execute( recaptcha_verified = Captcha::CaptchaVerificationService.new(spam_params: spam_params).execute
captcha_response: spam_params.captcha_response,
request: request
)
if recaptcha_verified if recaptcha_verified
# If it's a request which is already verified through CAPTCHA, # If it's a request which is already verified through CAPTCHA,
...@@ -73,10 +28,9 @@ module Spam ...@@ -73,10 +28,9 @@ module Spam
ServiceResponse.success(message: "CAPTCHA successfully verified") ServiceResponse.success(message: "CAPTCHA successfully verified")
else else
return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user) return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user)
return ServiceResponse.success(message: 'Skipped spam check because request was not present') unless request
return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam? return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?
perform_spam_service_check(spam_params.api) perform_spam_service_check
ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement") ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement")
end end
end end
...@@ -86,7 +40,7 @@ module Spam ...@@ -86,7 +40,7 @@ module Spam
private private
attr_reader :user, :action attr_reader :user, :action, :target, :spam_params, :spam_log
## ##
# In order to be proceed to the spam check process, the target must be # In order to be proceed to the spam check process, the target must be
...@@ -104,7 +58,7 @@ module Spam ...@@ -104,7 +58,7 @@ module Spam
## ##
# Performs the spam check using the spam verdict service, and modifies the target model # Performs the spam check using the spam verdict service, and modifies the target model
# accordingly based on the result. # accordingly based on the result.
def perform_spam_service_check(api) def perform_spam_service_check
ensure_target_is_dirty ensure_target_is_dirty
# since we can check for spam, and recaptcha is not verified, # since we can check for spam, and recaptcha is not verified,
...@@ -113,7 +67,7 @@ module Spam ...@@ -113,7 +67,7 @@ module Spam
case result case result
when CONDITIONAL_ALLOW when CONDITIONAL_ALLOW
# at the moment, this means "ask for reCAPTCHA" # at the moment, this means "ask for reCAPTCHA"
create_spam_log(api) create_spam_log
break if target.allow_possible_spam? break if target.allow_possible_spam?
...@@ -122,12 +76,12 @@ module Spam ...@@ -122,12 +76,12 @@ module Spam
# TODO: remove `unless target.allow_possible_spam?` once this flag has been passed to `SpamVerdictService` # TODO: remove `unless target.allow_possible_spam?` once this flag has been passed to `SpamVerdictService`
# https://gitlab.com/gitlab-org/gitlab/-/issues/214739 # https://gitlab.com/gitlab-org/gitlab/-/issues/214739
target.spam! unless target.allow_possible_spam? target.spam! unless target.allow_possible_spam?
create_spam_log(api) create_spam_log
when BLOCK_USER when BLOCK_USER
# TODO: improve BLOCK_USER handling, non-existent until now # TODO: improve BLOCK_USER handling, non-existent until now
# https://gitlab.com/gitlab-org/gitlab/-/issues/329666 # https://gitlab.com/gitlab-org/gitlab/-/issues/329666
target.spam! unless target.allow_possible_spam? target.spam! unless target.allow_possible_spam?
create_spam_log(api) create_spam_log
when ALLOW when ALLOW
target.clear_spam_flags! target.clear_spam_flags!
when NOOP when NOOP
...@@ -137,16 +91,21 @@ module Spam ...@@ -137,16 +91,21 @@ module Spam
end end
end end
def create_spam_log(api) def create_spam_log
@spam_log = SpamLog.create!( @spam_log = SpamLog.create!(
{ {
user_id: target.author_id, user_id: target.author_id,
title: target.spam_title, title: target.spam_title,
description: target.spam_description, description: target.spam_description,
source_ip: options[:ip_address], source_ip: spam_params.ip_address,
user_agent: options[:user_agent], user_agent: spam_params.user_agent,
noteable_type: noteable_type, noteable_type: noteable_type,
via_api: api # Now, all requests are via the API, so hardcode it to true to simplify the logic and API
# of this service. See https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2266
# for original introduction of `via_api` field.
# See discussion here about possibly deprecating this field:
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2266#note_542527450
via_api: true
} }
) )
...@@ -159,9 +118,14 @@ module Spam ...@@ -159,9 +118,14 @@ module Spam
target_type: noteable_type target_type: noteable_type
} }
options = {
ip_address: spam_params.ip_address,
user_agent: spam_params.user_agent,
referer: spam_params.referer
}
SpamVerdictService.new(target: target, SpamVerdictService.new(target: target,
user: user, user: user,
request: request,
options: options, options: options,
context: context) context: context)
end end
......
...@@ -3,30 +3,54 @@ ...@@ -3,30 +3,54 @@
module Spam module Spam
## ##
# This class is a Parameter Object (https://refactoring.com/catalog/introduceParameterObject.html) # This class is a Parameter Object (https://refactoring.com/catalog/introduceParameterObject.html)
# which acts as an container abstraction for multiple parameter values related to spam and # which acts as an container abstraction for multiple values related to spam and
# captcha processing for a request. # captcha processing for a provided HTTP request object.
#
# It is used to encapsulate these values and allow them to be passed from the Controller/GraphQL
# layers down into to the Service layer, without needing to pass the entire request and therefore
# unnecessarily couple the Service layer to the HTTP request.
# #
# Values contained are: # Values contained are:
# #
# api: A boolean flag indicating if the request was submitted via the REST or GraphQL API
# captcha_response: The response resulting from the user solving a captcha. Currently it is # captcha_response: The response resulting from the user solving a captcha. Currently it is
# a scalar reCAPTCHA response string, but it can be expanded to an object in the future to # a scalar reCAPTCHA response string, but it can be expanded to an object in the future to
# support other captcha implementations such as FriendlyCaptcha. # support other captcha implementations such as FriendlyCaptcha. Obtained from
# spam_log_id: The id of a SpamLog record. # request.headers['X-GitLab-Captcha-Response']
# spam_log_id: The id of a SpamLog record. Obtained from request.headers['X-GitLab-Spam-Log-Id']
# ip_address = The remote IP. Obtained from request.env['action_dispatch.remote_ip']
# user_agent = The user agent. Obtained from request.env['HTTP_USER_AGENT']
# referer = The HTTP referer. Obtained from request.env['HTTP_REFERER']
#
# NOTE: The presence of these values in the request is not currently enforced. If they are missing,
# then the spam check may fail, or the SpamLog or UserAgentDetail may have missing fields.
class SpamParams class SpamParams
attr_reader :api, :captcha_response, :spam_log_id def self.new_from_request(request:)
self.new(
captcha_response: request.headers['X-GitLab-Captcha-Response'],
spam_log_id: request.headers['X-GitLab-Spam-Log-Id'],
ip_address: request.env['action_dispatch.remote_ip'].to_s,
user_agent: request.env['HTTP_USER_AGENT'],
referer: request.env['HTTP_REFERER']
)
end
attr_reader :captcha_response, :spam_log_id, :ip_address, :user_agent, :referer
def initialize(api:, captcha_response:, spam_log_id:) def initialize(captcha_response:, spam_log_id:, ip_address:, user_agent:, referer:)
@api = api.present?
@captcha_response = captcha_response @captcha_response = captcha_response
@spam_log_id = spam_log_id @spam_log_id = spam_log_id
@ip_address = ip_address
@user_agent = user_agent
@referer = referer
end end
def ==(other) def ==(other)
other.class <= self.class && other.class <= self.class &&
other.api == api &&
other.captcha_response == captcha_response && other.captcha_response == captcha_response &&
other.spam_log_id == spam_log_id other.spam_log_id == spam_log_id &&
other.ip_address == ip_address &&
other.user_agent == user_agent &&
other.referer == referer
end end
end end
end end
...@@ -5,9 +5,8 @@ module Spam ...@@ -5,9 +5,8 @@ module Spam
include AkismetMethods include AkismetMethods
include SpamConstants include SpamConstants
def initialize(user:, target:, request:, options:, context: {}) def initialize(user:, target:, options:, context: {})
@target = target @target = target
@request = request
@user = user @user = user
@options = options @options = options
@context = context @context = context
...@@ -59,7 +58,7 @@ module Spam ...@@ -59,7 +58,7 @@ module Spam
private private
attr_reader :user, :target, :request, :options, :context attr_reader :user, :target, :options, :context
def akismet_verdict def akismet_verdict
if akismet.spam? if akismet.spam?
......
# frozen_string_literal: true # frozen_string_literal: true
class UserAgentDetailService class UserAgentDetailService
attr_accessor :spammable, :request def initialize(spammable:, spam_params:)
def initialize(spammable, request)
@spammable = spammable @spammable = spammable
@request = request @spam_params = spam_params
end end
def create def create
return unless request unless spam_params&.user_agent && spam_params&.ip_address
messasge = 'Skipped UserAgentDetail creation because necessary spam_params were not provided'
return ServiceResponse.success(message: messasge)
end
spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s) spammable.create_user_agent_detail(user_agent: spam_params.user_agent, ip_address: spam_params.ip_address)
end end
private
attr_reader :spammable, :spam_params
end end
...@@ -17,7 +17,8 @@ module Issues ...@@ -17,7 +17,8 @@ module Issues
confidential: true confidential: true
} }
issue = Issues::CreateService.new(project: @project, current_user: @current_user, params: issue_params).execute # NOTE: Intentionally not performing spam check, for now.
issue = Issues::CreateService.new(project: @project, current_user: @current_user, params: issue_params, spam_params: nil).execute
if issue.valid? if issue.valid?
success(issue) success(issue)
......
...@@ -22,7 +22,8 @@ module QualityManagement ...@@ -22,7 +22,8 @@ module QualityManagement
title: title, title: title,
description: description, description: description,
label_ids: label_ids label_ids: label_ids
} },
spam_params: nil
).execute ).execute
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid? return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
......
...@@ -4,6 +4,16 @@ module RequirementsManagement ...@@ -4,6 +4,16 @@ module RequirementsManagement
class CreateRequirementService < ::BaseProjectService class CreateRequirementService < ::BaseProjectService
include Gitlab::Allowable include Gitlab::Allowable
# NOTE: Even though this class does not (yet) do spam checking, this constructor takes a
# spam_params named argument in order to be consistent with the other issuable service
# constructors. This is necessary in order for methods such as create_issuable to be able to
# work in a consistent way with all different issuable services.
# See https://gitlab.com/groups/gitlab-org/-/epics/5527#current-vulnerabilities
# for more context.
def initialize(project:, current_user: nil, params: {}, spam_params: nil)
super(project: project, current_user: current_user, params: params)
end
def execute def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :create_requirement, project) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :create_requirement, project)
......
...@@ -57,7 +57,7 @@ class Gitlab::Seeder::Burndown ...@@ -57,7 +57,7 @@ class Gitlab::Seeder::Burndown
weight: rand(1..9) weight: rand(1..9)
} }
Issues::CreateService.new(project: @project, current_user: @project.team.users.sample, params: issue_params).execute Issues::CreateService.new(project: @project, current_user: @project.team.users.sample, params: issue_params, spam_params: nil).execute
end end
end end
......
...@@ -110,14 +110,16 @@ class Gitlab::Seeder::CustomizableCycleAnalytics ...@@ -110,14 +110,16 @@ class Gitlab::Seeder::CustomizableCycleAnalytics
Issues::UpdateService.new( Issues::UpdateService.new(
project: project, project: project,
current_user: user, current_user: user,
params: { label_ids: [in_dev_label.id] } params: { label_ids: [in_dev_label.id] },
spam_params: nil
).execute(issue) ).execute(issue)
Timecop.travel(random_duration_in_hours.hours.from_now) Timecop.travel(random_duration_in_hours.hours.from_now)
Issues::UpdateService.new( Issues::UpdateService.new(
project: project, project: project,
current_user: user, current_user: user,
params: { label_ids: [in_review_label.id] } params: { label_ids: [in_review_label.id] },
spam_params: nil
).execute(issue) ).execute(issue)
end end
end end
...@@ -153,7 +155,7 @@ class Gitlab::Seeder::CustomizableCycleAnalytics ...@@ -153,7 +155,7 @@ class Gitlab::Seeder::CustomizableCycleAnalytics
assignees: [project.team.users.sample] assignees: [project.team.users.sample]
} }
Issues::CreateService.new(project: @project, current_user: project.team.users.sample, params: issue_params).execute Issues::CreateService.new(project: @project, current_user: project.team.users.sample, params: issue_params, spam_params: nil).execute
end end
end end
......
...@@ -52,7 +52,7 @@ class Gitlab::Seeder::ProductivityAnalytics ...@@ -52,7 +52,7 @@ class Gitlab::Seeder::ProductivityAnalytics
} }
Timecop.travel rand(10).days.from_now do Timecop.travel rand(10).days.from_now do
Issues::CreateService.new(project: @project, current_user: @project.team.users.sample, params: issue_params).execute Issues::CreateService.new(project: @project, current_user: @project.team.users.sample, params: issue_params, spam_params: nil).execute
end end
end end
end end
......
...@@ -44,6 +44,7 @@ RSpec.describe Mutations::Issues::Create do ...@@ -44,6 +44,7 @@ RSpec.describe Mutations::Issues::Create do
project.add_guest(assignee1) project.add_guest(assignee1)
project.add_guest(assignee2) project.add_guest(assignee2)
stub_licensed_features(issuable_health_status: true) stub_licensed_features(issuable_health_status: true)
stub_spam_services
end end
subject { mutation.resolve(**mutation_params) } subject { mutation.resolve(**mutation_params) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Mutations::Issues::Update do RSpec.describe Mutations::Issues::Update do
let(:user) { create(:user) } let(:user) { create(:user) }
before do
stub_spam_services
end
it_behaves_like 'updating health status' do it_behaves_like 'updating health status' do
let(:resource) { create(:issue) } let(:resource) { create(:issue) }
end end
......
...@@ -8,7 +8,7 @@ RSpec.describe Issues::CreateService do ...@@ -8,7 +8,7 @@ RSpec.describe Issues::CreateService do
let_it_be_with_reload(:project) { create(:project, group: group) } let_it_be_with_reload(:project) { create(:project, group: group) }
let(:params) { { title: 'Awesome issue', description: 'please fix', weight: 9 } } let(:params) { { title: 'Awesome issue', description: 'please fix', weight: 9 } }
let(:service) { described_class.new(project: project, current_user: user, params: params) } let(:service) { described_class.new(project: project, current_user: user, params: params, spam_params: nil) }
describe '#execute' do describe '#execute' do
context 'when current user cannot admin issues in the project' do context 'when current user cannot admin issues in the project' do
...@@ -110,7 +110,7 @@ RSpec.describe Issues::CreateService do ...@@ -110,7 +110,7 @@ RSpec.describe Issues::CreateService do
confidential_epic = create(:epic, group: group, confidential: true) confidential_epic = create(:epic, group: group, confidential: true)
params = { title: 'confidential issue', epic_id: confidential_epic.id } params = { title: 'confidential issue', epic_id: confidential_epic.id }
issue = described_class.new(project: project, current_user: user, params: params).execute issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute
expect(issue.confidential).to eq(true) expect(issue.confidential).to eq(true)
end end
...@@ -120,7 +120,7 @@ RSpec.describe Issues::CreateService do ...@@ -120,7 +120,7 @@ RSpec.describe Issues::CreateService do
it 'creates a confidential child issue' do it 'creates a confidential child issue' do
params = { title: 'confidential issue', epic_id: epic.id, confidential: true } params = { title: 'confidential issue', epic_id: epic.id, confidential: true }
issue = described_class.new(project: project, current_user: user, params: params).execute issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute
expect(issue.confidential).to eq(true) expect(issue.confidential).to eq(true)
end end
......
...@@ -28,9 +28,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do ...@@ -28,9 +28,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do
context 'when using label_ids parameter' do context 'when using label_ids parameter' do
it 'adds only last selected exclusive scoped label' do it 'adds only last selected exclusive scoped label' do
issuable = described_class.new( args = { **described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id] } }
**described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id] } args[:spam_params] = nil if described_class.private_instance_methods.include?(:spam_params)
).execute issuable = described_class.new(**args).execute
expect(issuable.labels).to match_array([label1, label2]) expect(issuable.labels).to match_array([label1, label2])
end end
...@@ -38,9 +38,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do ...@@ -38,9 +38,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do
context 'when using labels parameter' do context 'when using labels parameter' do
it 'adds only last selected exclusive scoped label' do it 'adds only last selected exclusive scoped label' do
issuable = described_class.new( args = { **described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', labels: [label1.title, label3.title, label4.title, label2.title] } }
**described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', labels: [label1.title, label3.title, label4.title, label2.title] } args[:spam_params] = nil if described_class.private_instance_methods.include?(:spam_params)
).execute issuable = described_class.new(**args).execute
expect(issuable.labels).to match_array([label1, label2]) expect(issuable.labels).to match_array([label1, label2])
end end
...@@ -58,9 +58,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do ...@@ -58,9 +58,9 @@ RSpec.shared_examples 'new issuable with scoped labels' do
label3 = create_label('key::label2') label3 = create_label('key::label2')
label4 = create_label('key::label3') label4 = create_label('key::label3')
issuable = described_class.new( args = { **described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id] } }
**described_class.constructor_container_arg(parent), current_user: user, params: { title: 'test', label_ids: [label1.id, label3.id, label4.id, label2.id] } args[:spam_params] = nil if described_class.private_instance_methods.include?(:spam_params)
).execute issuable = described_class.new(**args).execute
expect(issuable.labels).to match_array([label1, label2, label3, label4]) expect(issuable.labels).to match_array([label1, label2, label3, label4])
end end
......
...@@ -577,10 +577,6 @@ module API ...@@ -577,10 +577,6 @@ module API
Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}")
end end
def with_api_params(&block)
yield({ api: true, request: request })
end
protected protected
def project_finder_params_visibility_ce def project_finder_params_visibility_ce
......
...@@ -72,22 +72,18 @@ module API ...@@ -72,22 +72,18 @@ module API
end end
def process_create_params(args) def process_create_params(args)
with_api_params do |api_params|
args[:snippet_actions] = args.delete(:files)&.map do |file| args[:snippet_actions] = args.delete(:files)&.map do |file|
file[:action] = :create file[:action] = :create
file.symbolize_keys file.symbolize_keys
end end
args.merge(api_params) args
end
end end
def process_update_params(args) def process_update_params(args)
with_api_params do |api_params|
args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys) args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
args.merge(api_params) args
end
end end
def validate_params_for_multiple_files(snippet) def validate_params_for_multiple_files(snippet)
......
...@@ -255,9 +255,11 @@ module API ...@@ -255,9 +255,11 @@ module API
issue_params = convert_parameters_from_legacy_format(issue_params) issue_params = convert_parameters_from_legacy_format(issue_params)
begin begin
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::CreateService.new(project: user_project, issue = ::Issues::CreateService.new(project: user_project,
current_user: current_user, current_user: current_user,
params: issue_params.merge(request: request, api: true)).execute params: issue_params,
spam_params: spam_params).execute
if issue.spam? if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400) render_api_error!({ error: 'Spam detected' }, 400)
...@@ -294,13 +296,15 @@ module API ...@@ -294,13 +296,15 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue authorize! :update_issue, issue
update_params = declared_params(include_missing: false).merge(request: request, api: true) update_params = declared_params(include_missing: false)
update_params = convert_parameters_from_legacy_format(update_params) update_params = convert_parameters_from_legacy_format(update_params)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::UpdateService.new(project: user_project, issue = ::Issues::UpdateService.new(project: user_project,
current_user: current_user, current_user: current_user,
params: update_params).execute(issue) params: update_params,
spam_params: spam_params).execute(issue)
render_spam_error! if issue.spam? render_spam_error! if issue.spam?
......
...@@ -75,7 +75,8 @@ module API ...@@ -75,7 +75,8 @@ module API
snippet_params = process_create_params(declared_params(include_missing: false)) snippet_params = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params).execute spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
if service_response.success? if service_response.success?
...@@ -116,7 +117,8 @@ module API ...@@ -116,7 +117,8 @@ module API
snippet_params = process_update_params(declared_params(include_missing: false)) snippet_params = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params).execute(snippet) spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute(snippet)
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
if service_response.success? if service_response.success?
......
...@@ -84,7 +84,8 @@ module API ...@@ -84,7 +84,8 @@ module API
attrs = process_create_params(declared_params(include_missing: false)) attrs = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs).execute spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
if service_response.success? if service_response.success?
...@@ -126,7 +127,8 @@ module API ...@@ -126,7 +127,8 @@ module API
attrs = process_update_params(declared_params(include_missing: false)) attrs = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs).execute(snippet) spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute(snippet)
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
......
...@@ -61,7 +61,8 @@ module Gitlab ...@@ -61,7 +61,8 @@ module Gitlab
params: { params: {
title: mail.subject, title: mail.subject,
description: message_including_reply description: message_including_reply
} },
spam_params: nil
).execute ).execute
end end
......
...@@ -83,7 +83,8 @@ module Gitlab ...@@ -83,7 +83,8 @@ module Gitlab
description: message_including_template, description: message_including_template,
confidential: true, confidential: true,
external_author: from_address external_author: from_address
} },
spam_params: nil
).execute ).execute
raise InvalidIssueError unless @issue.persisted? raise InvalidIssueError unless @issue.persisted?
......
...@@ -33,7 +33,7 @@ module Gitlab ...@@ -33,7 +33,7 @@ module Gitlab
private private
def create_issue(title:, description:) def create_issue(title:, description:)
Issues::CreateService.new(project: project, current_user: current_user, params: { title: title, description: description }).execute Issues::CreateService.new(project: project, current_user: current_user, params: { title: title, description: description }, spam_params: nil).execute
end end
def presenter(issue) def presenter(issue)
......
...@@ -30,7 +30,7 @@ module Quality ...@@ -30,7 +30,7 @@ module Quality
labels: labels.join(',') labels: labels.join(',')
} }
params[:closed_at] = params[:created_at] + rand(35).days if params[:state] == 'closed' params[:closed_at] = params[:created_at] + rand(35).days if params[:state] == 'closed'
issue = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params).execute issue = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params, spam_params: nil).execute
if issue.persisted? if issue.persisted?
created_issues_count += 1 created_issues_count += 1
......
...@@ -145,7 +145,7 @@ RSpec.describe 'Contributions Calendar', :js do ...@@ -145,7 +145,7 @@ RSpec.describe 'Contributions Calendar', :js do
describe '1 issue creation calendar activity' do describe '1 issue creation calendar activity' do
before do before do
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params).execute Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
end end
it_behaves_like 'a day with activity', contribution_count: 1 it_behaves_like 'a day with activity', contribution_count: 1
...@@ -180,7 +180,7 @@ RSpec.describe 'Contributions Calendar', :js do ...@@ -180,7 +180,7 @@ RSpec.describe 'Contributions Calendar', :js do
push_code_contribution push_code_contribution
travel_to(Date.yesterday) do travel_to(Date.yesterday) do
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params).execute Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
end end
end end
include_context 'visit user page' include_context 'visit user page'
......
...@@ -9,7 +9,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do ...@@ -9,7 +9,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do
let(:author) { create(:user) } let(:author) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } } let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params).execute } let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params, spam_params: nil).execute }
let(:mail) { ActionMailer::Base.deliveries.last } let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) } let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
......
...@@ -125,7 +125,7 @@ RSpec.describe 'Users > User browses projects on user page', :js do ...@@ -125,7 +125,7 @@ RSpec.describe 'Users > User browses projects on user page', :js do
end end
before do before do
Issues::CreateService.new(project: contributed_project, current_user: user, params: { title: 'Bug in old browser' }).execute Issues::CreateService.new(project: contributed_project, current_user: user, params: { title: 'Bug in old browser' }, spam_params: nil).execute
event = create(:push_event, project: contributed_project, author: user) event = create(:push_event, project: contributed_project, author: user)
create(:push_event_payload, event: event, commit_count: 3) create(:push_event_payload, event: event, commit_count: 3)
end end
......
...@@ -50,6 +50,7 @@ RSpec.describe Mutations::Issues::Create do ...@@ -50,6 +50,7 @@ RSpec.describe Mutations::Issues::Create do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.add_guest(assignee1) project.add_guest(assignee1)
project.add_guest(assignee2) project.add_guest(assignee2)
stub_spam_services
end end
subject { mutation.resolve(**mutation_params) } subject { mutation.resolve(**mutation_params) }
......
...@@ -17,6 +17,10 @@ RSpec.describe Mutations::Issues::SetConfidential do ...@@ -17,6 +17,10 @@ RSpec.describe Mutations::Issues::SetConfidential do
subject { mutation.resolve(project_path: project.full_path, iid: issue.iid, confidential: confidential) } subject { mutation.resolve(project_path: project.full_path, iid: issue.iid, confidential: confidential) }
before do
stub_spam_services
end
it_behaves_like 'permission level for issue mutation is correctly verified' it_behaves_like 'permission level for issue mutation is correctly verified'
context 'when the user can update the issue' do context 'when the user can update the issue' do
......
...@@ -35,6 +35,10 @@ RSpec.describe Mutations::Issues::Update do ...@@ -35,6 +35,10 @@ RSpec.describe Mutations::Issues::Update do
subject { mutation.resolve(**mutation_params) } subject { mutation.resolve(**mutation_params) }
before do
stub_spam_services
end
it_behaves_like 'permission level for issue mutation is correctly verified' it_behaves_like 'permission level for issue mutation is correctly verified'
context 'when the user can update the issue' do context 'when the user can update the issue' do
......
...@@ -74,7 +74,7 @@ RSpec.describe Integrations::MicrosoftTeams do ...@@ -74,7 +74,7 @@ RSpec.describe Integrations::MicrosoftTeams do
context 'with issue events' do context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } } let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do let(:issues_sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts) service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute issue = service.execute
service.hook_data(issue, 'open') service.hook_data(issue, 'open')
end end
......
...@@ -17,7 +17,6 @@ RSpec.describe 'Creating a Snippet' do ...@@ -17,7 +17,6 @@ RSpec.describe 'Creating a Snippet' do
let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] } let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] }
let(:project_path) { nil } let(:project_path) { nil }
let(:uploaded_files) { nil } let(:uploaded_files) { nil }
let(:spam_mutation_vars) { {} }
let(:mutation_vars) do let(:mutation_vars) do
{ {
description: description, description: description,
...@@ -26,7 +25,7 @@ RSpec.describe 'Creating a Snippet' do ...@@ -26,7 +25,7 @@ RSpec.describe 'Creating a Snippet' do
project_path: project_path, project_path: project_path,
uploaded_files: uploaded_files, uploaded_files: uploaded_files,
blob_actions: actions blob_actions: actions
}.merge(spam_mutation_vars) }
end end
let(:mutation) do let(:mutation) do
...@@ -77,21 +76,6 @@ RSpec.describe 'Creating a Snippet' do ...@@ -77,21 +76,6 @@ RSpec.describe 'Creating a Snippet' do
expect(mutation_response['snippet']).to be_nil expect(mutation_response['snippet']).to be_nil
end end
context 'when snippet_spam flag is disabled' do
before do
stub_feature_flags(snippet_spam: false)
end
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::CreateService)
.to receive(:new)
.with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
end
end
end end
shared_examples 'creates snippet' do shared_examples 'creates snippet' do
...@@ -121,15 +105,6 @@ RSpec.describe 'Creating a Snippet' do ...@@ -121,15 +105,6 @@ RSpec.describe 'Creating a Snippet' do
it_behaves_like 'snippet edit usage data counters' it_behaves_like 'snippet edit usage data counters'
it_behaves_like 'a mutation which can mutate a spammable' do it_behaves_like 'a mutation which can mutate a spammable' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1234 }
let(:spam_mutation_vars) do
{
captcha_response: captcha_response,
spam_log_id: spam_log_id
}
end
let(:service) { Snippets::CreateService } let(:service) { Snippets::CreateService }
end end
end end
...@@ -190,7 +165,7 @@ RSpec.describe 'Creating a Snippet' do ...@@ -190,7 +165,7 @@ RSpec.describe 'Creating a Snippet' do
it do it do
expect(::Snippets::CreateService).to receive(:new) expect(::Snippets::CreateService).to receive(:new)
.with(project: nil, current_user: user, params: hash_including(files: expected_value)) .with(project: nil, current_user: user, params: hash_including(files: expected_value), spam_params: instance_of(::Spam::SpamParams))
.and_return(double(execute: creation_response)) .and_return(double(execute: creation_response))
subject subject
......
...@@ -16,7 +16,6 @@ RSpec.describe 'Updating a Snippet' do ...@@ -16,7 +16,6 @@ RSpec.describe 'Updating a Snippet' do
let(:updated_file) { 'CHANGELOG' } let(:updated_file) { 'CHANGELOG' }
let(:deleted_file) { 'README' } let(:deleted_file) { 'README' }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s } let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:spam_mutation_vars) { {} }
let(:mutation_vars) do let(:mutation_vars) do
{ {
id: snippet_gid, id: snippet_gid,
...@@ -27,7 +26,7 @@ RSpec.describe 'Updating a Snippet' do ...@@ -27,7 +26,7 @@ RSpec.describe 'Updating a Snippet' do
{ action: :update, filePath: updated_file, content: updated_content }, { action: :update, filePath: updated_file, content: updated_content },
{ action: :delete, filePath: deleted_file } { action: :delete, filePath: deleted_file }
] ]
}.merge(spam_mutation_vars) }
end end
let(:mutation) do let(:mutation) do
...@@ -82,21 +81,6 @@ RSpec.describe 'Updating a Snippet' do ...@@ -82,21 +81,6 @@ RSpec.describe 'Updating a Snippet' do
end end
end end
context 'when snippet_spam flag is disabled' do
before do
stub_feature_flags(snippet_spam: false)
end
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::UpdateService)
.to receive(:new)
.with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
end
end
context 'when there are ActiveRecord validation errors' do context 'when there are ActiveRecord validation errors' do
let(:updated_title) { '' } let(:updated_title) { '' }
...@@ -125,15 +109,6 @@ RSpec.describe 'Updating a Snippet' do ...@@ -125,15 +109,6 @@ RSpec.describe 'Updating a Snippet' do
end end
it_behaves_like 'a mutation which can mutate a spammable' do it_behaves_like 'a mutation which can mutate a spammable' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1234 }
let(:spam_mutation_vars) do
{
captcha_response: captcha_response,
spam_log_id: spam_log_id
}
end
let(:service) { Snippets::UpdateService } let(:service) { Snippets::UpdateService }
end end
......
...@@ -4,21 +4,31 @@ require 'spec_helper' ...@@ -4,21 +4,31 @@ require 'spec_helper'
RSpec.describe Captcha::CaptchaVerificationService do RSpec.describe Captcha::CaptchaVerificationService do
describe '#execute' do describe '#execute' do
let(:captcha_response) { nil } let(:captcha_response) { 'abc123' }
let(:request) { double(:request) } let(:fake_ip) { '1.2.3.4' }
let(:service) { described_class.new } let(:spam_params) do
::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: double,
ip_address: fake_ip,
user_agent: double,
referer: double
)
end
let(:service) { described_class.new(spam_params: spam_params) }
subject { service.execute(captcha_response: captcha_response, request: request) } subject { service.execute }
context 'when there is no captcha_response' do context 'when there is no captcha_response' do
let(:captcha_response) { nil }
it 'returns false' do it 'returns false' do
expect(subject).to eq(false) expect(subject).to eq(false)
end end
end end
context 'when there is a captcha_response' do context 'when there is a captcha_response' do
let(:captcha_response) { 'abc123' }
before do before do
expect(Gitlab::Recaptcha).to receive(:load_configurations!) expect(Gitlab::Recaptcha).to receive(:load_configurations!)
end end
...@@ -29,10 +39,12 @@ RSpec.describe Captcha::CaptchaVerificationService do ...@@ -29,10 +39,12 @@ RSpec.describe Captcha::CaptchaVerificationService do
expect(subject).to eq(true) expect(subject).to eq(true)
end end
it 'has a request method which returns the request' do it 'has a request method which returns an object with the ip address #remote_ip' do
subject subject
expect(service.send(:request)).to eq(request) request_struct = service.send(:request)
expect(request_struct).to respond_to(:remote_ip)
expect(request_struct.remote_ip).to eq(fake_ip)
end end
end end
end end
......
This diff is collapsed.
...@@ -19,8 +19,9 @@ RSpec.describe Snippets::CreateService do ...@@ -19,8 +19,9 @@ RSpec.describe Snippets::CreateService do
let(:extra_opts) { {} } let(:extra_opts) { {} }
let(:creator) { admin } let(:creator) { admin }
let(:spam_params) { double }
subject { described_class.new(project: project, current_user: creator, params: opts).execute } subject { described_class.new(project: project, current_user: creator, params: opts, spam_params: spam_params).execute }
let(:snippet) { subject.payload[:snippet] } let(:snippet) { subject.payload[:snippet] }
...@@ -301,6 +302,10 @@ RSpec.describe Snippets::CreateService do ...@@ -301,6 +302,10 @@ RSpec.describe Snippets::CreateService do
end end
end end
before do
stub_spam_services
end
context 'when ProjectSnippet' do context 'when ProjectSnippet' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
......
...@@ -20,7 +20,9 @@ RSpec.describe Snippets::UpdateService do ...@@ -20,7 +20,9 @@ RSpec.describe Snippets::UpdateService do
let(:extra_opts) { {} } let(:extra_opts) { {} }
let(:options) { base_opts.merge(extra_opts) } let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user } let(:updater) { user }
let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options) } let(:spam_params) { double }
let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options, spam_params: spam_params) }
subject { service.execute(snippet) } subject { service.execute(snippet) }
...@@ -721,6 +723,10 @@ RSpec.describe Snippets::UpdateService do ...@@ -721,6 +723,10 @@ RSpec.describe Snippets::UpdateService do
end end
end end
before do
stub_spam_services
end
context 'when Project Snippet' do context 'when Project Snippet' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :repository, author: user, project: project) } let!(:snippet) { create(:project_snippet, :repository, author: user, project: project) }
......
...@@ -59,7 +59,7 @@ RSpec.describe Spam::AkismetService do ...@@ -59,7 +59,7 @@ RSpec.describe Spam::AkismetService do
it_behaves_like 'no activity if Akismet is not enabled', :spam?, :check it_behaves_like 'no activity if Akismet is not enabled', :spam?, :check
context 'if Akismet is enabled' do context 'if Akismet is enabled' do
it 'correctly transforms options for the akismet client' do it 'correctly transforms options for the akismet client, including spelling of referrer key' do
expected_check_params = { expected_check_params = {
type: 'comment', type: 'comment',
text: text, text: text,
......
...@@ -5,15 +5,20 @@ require 'spec_helper' ...@@ -5,15 +5,20 @@ require 'spec_helper'
RSpec.describe Spam::SpamActionService do RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants' include_context 'includes Spam constants'
let(:request) { double(:request, env: env, headers: {}) }
let(:issue) { create(:issue, project: project, author: user) } let(:issue) { create(:issue, project: project, author: user) }
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' } let(:fake_referer) { 'fake-http-referer' }
let(:env) do let(:captcha_response) { 'abc123' }
{ 'action_dispatch.remote_ip' => fake_ip, let(:spam_log_id) { existing_spam_log.id }
'HTTP_USER_AGENT' => fake_user_agent, let(:spam_params) do
'HTTP_REFERER' => fake_referer } ::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: spam_log_id,
ip_address: fake_ip,
user_agent: fake_user_agent,
referer: fake_referer
)
end end
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
...@@ -23,32 +28,33 @@ RSpec.describe Spam::SpamActionService do ...@@ -23,32 +28,33 @@ RSpec.describe Spam::SpamActionService do
issue.spam = false issue.spam = false
end end
shared_examples 'only checks for spam if a request is provided' do describe 'constructor argument validation' do
context 'when request is missing' do subject do
let(:request) { nil } described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
described_service.execute
end
it "doesn't check as spam" do context 'when spam_params is nil' do
expect(fake_verdict_service).not_to receive(:execute) let(:spam_params) { nil }
let(:expected_service_params_not_present_message) do
/Skipped spam check because spam_params was not present/
end
it "returns success with a messaage" do
response = subject response = subject
expect(response.message).to match(/request was not present/) expect(response.message).to match(expected_service_params_not_present_message)
expect(issue).not_to be_spam expect(issue).not_to be_spam
end end
end end
context 'when request exists' do
it 'creates a spam log' do
expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
end
end
end end
shared_examples 'creates a spam log' do shared_examples 'creates a spam log' do
it do it do
expect { subject }.to change(SpamLog, :count).by(1) expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
new_spam_log = SpamLog.last new_spam_log = SpamLog.last
expect(new_spam_log.user_id).to eq(user.id) expect(new_spam_log.user_id).to eq(user.id)
expect(new_spam_log.title).to eq(issue.title) expect(new_spam_log.title).to eq(issue.title)
...@@ -56,25 +62,14 @@ RSpec.describe Spam::SpamActionService do ...@@ -56,25 +62,14 @@ RSpec.describe Spam::SpamActionService do
expect(new_spam_log.source_ip).to eq(fake_ip) expect(new_spam_log.source_ip).to eq(fake_ip)
expect(new_spam_log.user_agent).to eq(fake_user_agent) expect(new_spam_log.user_agent).to eq(fake_user_agent)
expect(new_spam_log.noteable_type).to eq('Issue') expect(new_spam_log.noteable_type).to eq('Issue')
expect(new_spam_log.via_api).to eq(false) expect(new_spam_log.via_api).to eq(true)
end end
end end
describe '#execute' do describe '#execute' do
let(:request) { double(:request, env: env, headers: nil) }
let(:fake_captcha_verification_service) { double(:captcha_verification_service) } let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) } let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false } let(:allowlisted) { false }
let(:api) { nil }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { existing_spam_log.id }
let(:spam_params) do
::Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
end
let(:verdict_service_opts) do let(:verdict_service_opts) do
{ {
...@@ -88,7 +83,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -88,7 +83,6 @@ RSpec.describe Spam::SpamActionService do
{ {
target: issue, target: issue,
user: user, user: user,
request: request,
options: verdict_service_opts, options: verdict_service_opts,
context: { context: {
action: :create, action: :create,
...@@ -100,40 +94,20 @@ RSpec.describe Spam::SpamActionService do ...@@ -100,40 +94,20 @@ RSpec.describe Spam::SpamActionService do
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) } let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do subject do
described_service = described_class.new(spammable: issue, request: request, user: user, action: :create) described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted) allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute(spam_params: spam_params) described_service.execute
end end
before do before do
allow(Captcha::CaptchaVerificationService).to receive(:new) { fake_captcha_verification_service } allow(Captcha::CaptchaVerificationService).to receive(:new).with(spam_params: spam_params) { fake_captcha_verification_service }
allow(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args).and_return(fake_verdict_service) allow(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args).and_return(fake_verdict_service)
end end
context 'when the captcha params are passed in the headers' do
let(:request) { double(:request, env: env, headers: headers) }
let(:spam_params) { Spam::SpamActionService.filter_spam_params!({ api: api }, request) }
let(:headers) do
{
'X-GitLab-Captcha-Response' => captcha_response,
'X-GitLab-Spam-Log-Id' => spam_log_id
}
end
it 'extracts the headers correctly' do
expect(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(true)
expect(SpamLog)
.to receive(:verify_recaptcha!).with(user_id: user.id, id: spam_log_id)
subject
end
end
context 'when captcha response verification returns true' do context 'when captcha response verification returns true' do
before do before do
allow(fake_captcha_verification_service) allow(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(true) .to receive(:execute).and_return(true)
end end
it "doesn't check with the SpamVerdictService" do it "doesn't check with the SpamVerdictService" do
...@@ -156,7 +130,7 @@ RSpec.describe Spam::SpamActionService do ...@@ -156,7 +130,7 @@ RSpec.describe Spam::SpamActionService do
context 'when captcha response verification returns false' do context 'when captcha response verification returns false' do
before do before do
allow(fake_captcha_verification_service) allow(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(false) .to receive(:execute).and_return(false)
end end
context 'when spammable attributes have not changed' do context 'when spammable attributes have not changed' do
...@@ -200,8 +174,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -200,8 +174,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
end end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do it 'marks as spam' do
response = subject response = subject
...@@ -211,8 +183,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -211,8 +183,6 @@ RSpec.describe Spam::SpamActionService do
end end
context 'when allow_possible_spam feature flag is true' do context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do it 'does not mark as spam' do
response = subject response = subject
...@@ -232,8 +202,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -232,8 +202,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
end end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do it 'marks as spam' do
response = subject response = subject
...@@ -243,8 +211,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -243,8 +211,6 @@ RSpec.describe Spam::SpamActionService do
end end
context 'when allow_possible_spam feature flag is true' do context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do it 'does not mark as spam' do
response = subject response = subject
...@@ -264,8 +230,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -264,8 +230,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
end end
it_behaves_like 'only checks for spam if a request is provided'
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log'
it 'does not mark as spam' do it 'does not mark as spam' do
...@@ -284,8 +248,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -284,8 +248,6 @@ RSpec.describe Spam::SpamActionService do
end end
context 'when allow_possible_spam feature flag is true' do context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log'
it 'does not mark as needing reCAPTCHA' do it 'does not mark as needing reCAPTCHA' do
...@@ -334,32 +296,6 @@ RSpec.describe Spam::SpamActionService do ...@@ -334,32 +296,6 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(ALLOW) allow(fake_verdict_service).to receive(:execute).and_return(ALLOW)
end end
context 'when the request is nil' do
let(:request) { nil }
let(:issue_ip_address) { '1.2.3.4' }
let(:issue_user_agent) { 'lynx' }
let(:verdict_service_opts) do
{
ip_address: issue_ip_address,
user_agent: issue_user_agent
}
end
before do
allow(issue).to receive(:ip_address) { issue_ip_address }
allow(issue).to receive(:user_agent) { issue_user_agent }
end
it 'assembles the options with information from the spammable' do
# TODO: This code untestable, because we do not perform a verification if there is not a
# request. See corresponding comment in code
# expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
subject
end
end
context 'when the request is present' do
it 'assembles the options with information from the request' do it 'assembles the options with information from the request' do
expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args) expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
...@@ -369,5 +305,4 @@ RSpec.describe Spam::SpamActionService do ...@@ -369,5 +305,4 @@ RSpec.describe Spam::SpamActionService do
end end
end end
end end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Spam::SpamParams do
describe '.new_from_request' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 42 }
let(:ip_address) { '0.0.0.0' }
let(:user_agent) { 'Lynx' }
let(:referer) { 'http://localhost' }
let(:headers) do
{
'X-GitLab-Captcha-Response' => captcha_response,
'X-GitLab-Spam-Log-Id' => spam_log_id
}
end
let(:env) do
{
'action_dispatch.remote_ip' => ip_address,
'HTTP_USER_AGENT' => user_agent,
'HTTP_REFERER' => referer
}
end
let(:request) {double(:request, headers: headers, env: env)}
it 'constructs from a request' do
expected = ::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: spam_log_id,
ip_address: ip_address,
user_agent: user_agent,
referer: referer
)
expect(described_class.new_from_request(request: request)).to eq(expected)
end
end
end
...@@ -14,13 +14,11 @@ RSpec.describe Spam::SpamVerdictService do ...@@ -14,13 +14,11 @@ RSpec.describe Spam::SpamVerdictService do
'HTTP_REFERER' => fake_referer } 'HTTP_REFERER' => fake_referer }
end end
let(:request) { double(:request, env: env) }
let(:check_for_spam) { true } let(:check_for_spam) { true }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) } let_it_be(:issue) { create(:issue, author: user) }
let(:service) do let(:service) do
described_class.new(user: user, target: issue, request: request, options: {}) described_class.new(user: user, target: issue, options: {})
end end
let(:attribs) do let(:attribs) do
......
...@@ -190,6 +190,7 @@ RSpec.configure do |config| ...@@ -190,6 +190,7 @@ RSpec.configure do |config|
config.include RailsHelpers config.include RailsHelpers
config.include SidekiqMiddleware config.include SidekiqMiddleware
config.include StubActionCableConnection, type: :channel config.include StubActionCableConnection, type: :channel
config.include StubSpamServices
include StubFeatureFlags include StubFeatureFlags
......
# frozen_string_literal: true
module StubSpamServices
def stub_spam_services
allow(::Spam::SpamParams).to receive(:new_from_request) do
::Spam::SpamParams.new(
captcha_response: double(:captcha_response),
spam_log_id: double(:spam_log_id),
ip_address: double(:ip_address),
user_agent: double(:user_agent),
referer: double(:referer)
)
end
allow_next_instance_of(::Spam::SpamActionService) do |service|
allow(service).to receive(:execute)
end
allow_next_instance_of(::UserAgentDetailService) do |service|
allow(service).to receive(:create)
end
end
end
...@@ -3,17 +3,13 @@ ...@@ -3,17 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.shared_examples 'a mutation which can mutate a spammable' do RSpec.shared_examples 'a mutation which can mutate a spammable' do
describe "#additional_spam_params" do describe "#spam_params" do
it 'passes additional spam params to the service' do it 'passes spam params to the service constructor' do
args = [ args = [
project: anything, project: anything,
current_user: anything, current_user: anything,
params: hash_including( params: anything,
api: true, spam_params: instance_of(::Spam::SpamParams)
request: instance_of(ActionDispatch::Request),
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
] ]
expect(service).to receive(:new).with(*args).and_call_original expect(service).to receive(:new).with(*args).and_call_original
......
...@@ -57,7 +57,7 @@ RSpec.shared_examples 'has spam protection' do ...@@ -57,7 +57,7 @@ RSpec.shared_examples 'has spam protection' do
context 'and no CAPTCHA is required' do context 'and no CAPTCHA is required' do
let(:render_captcha) { false } let(:render_captcha) { false }
it 'does not return a to-level error' do it 'does not return a top-level error' do
send_request send_request
expect(graphql_errors).to be_blank expect(graphql_errors).to be_blank
......
...@@ -163,7 +163,7 @@ RSpec.shared_examples "chat integration" do |integration_name| ...@@ -163,7 +163,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with issue events" do context "with issue events" do
let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do let(:sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts) service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute issue = service.execute
service.hook_data(issue, "open") service.hook_data(issue, "open")
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'checking spam' do RSpec.shared_examples 'checking spam' do
let(:request) { double(:request, headers: headers) }
let(:headers) { nil }
let(:api) { true }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1 }
let(:disable_spam_action_service) { false }
let(:extra_opts) do
{
request: request,
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id,
disable_spam_action_service: disable_spam_action_service
}
end
before do before do
allow_next_instance_of(UserAgentDetailService) do |instance| allow_next_instance_of(UserAgentDetailService) do |instance|
allow(instance).to receive(:create) allow(instance).to receive(:create)
...@@ -25,67 +8,26 @@ RSpec.shared_examples 'checking spam' do ...@@ -25,67 +8,26 @@ RSpec.shared_examples 'checking spam' do
end end
it 'executes SpamActionService' do it 'executes SpamActionService' do
spam_params = Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
expect_next_instance_of( expect_next_instance_of(
Spam::SpamActionService, Spam::SpamActionService,
{ {
spammable: kind_of(Snippet), spammable: kind_of(Snippet),
request: request, spam_params: spam_params,
user: an_instance_of(User), user: an_instance_of(User),
action: action action: action
} }
) do |instance| ) do |instance|
expect(instance).to receive(:execute).with(spam_params: spam_params) expect(instance).to receive(:execute)
end end
subject subject
end end
context 'when CAPTCHA arguments are passed in the headers' do context 'when snippet_spam flag is disabled' do
let(:headers) do before do
{ stub_feature_flags(snippet_spam: false)
'X-GitLab-Spam-Log-Id' => spam_log_id,
'X-GitLab-Captcha-Response' => captcha_response
}
end
let(:extra_opts) do
{
request: request,
api: api,
disable_spam_action_service: disable_spam_action_service
}
end
it 'executes the SpamActionService correctly' do
spam_params = Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
expect_next_instance_of(
Spam::SpamActionService,
{
spammable: kind_of(Snippet),
request: request,
user: an_instance_of(User),
action: action
}
) do |instance|
expect(instance).to receive(:execute).with(spam_params: spam_params)
end
subject
end
end end
context 'when spam action service is disabled' do
let(:disable_spam_action_service) { true }
it 'request parameter is not passed to the service' do it 'request parameter is not passed to the service' do
expect(Spam::SpamActionService).not_to receive(:new) expect(Spam::SpamActionService).not_to receive(:new)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment