summaryrefslogtreecommitdiff
path: root/app/graphql/mutations/concerns/mutations/spam_protection.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/graphql/mutations/concerns/mutations/spam_protection.rb')
-rw-r--r--app/graphql/mutations/concerns/mutations/spam_protection.rb67
1 files changed, 67 insertions, 0 deletions
diff --git a/app/graphql/mutations/concerns/mutations/spam_protection.rb b/app/graphql/mutations/concerns/mutations/spam_protection.rb
new file mode 100644
index 00000000000..d765da23a4b
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/spam_protection.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Mutations
+ # This concern can be mixed into a mutation to provide support for spam checking,
+ # and optionally support the workflow to allow clients to display and solve CAPTCHAs.
+ module SpamProtection
+ extend ActiveSupport::Concern
+ include Spam::Concerns::HasSpamActionResponseFields
+
+ SpamActionError = Class.new(GraphQL::ExecutionError)
+ NeedsCaptchaResponseError = Class.new(SpamActionError)
+ SpamDisallowedError = Class.new(SpamActionError)
+
+ NEEDS_CAPTCHA_RESPONSE_MESSAGE = "Request denied. Solve CAPTCHA challenge and retry"
+ SPAM_DISALLOWED_MESSAGE = "Request denied. Spam detected"
+
+ 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)
+ fields = spam_action_response_fields(object)
+
+ # If the SpamActionService detected something as spam,
+ # this is non-recoverable and the needs_captcha_response
+ # should not be considered
+ kind = if fields[:spam]
+ :spam
+ elsif fields[:needs_captcha_response]
+ :needs_captcha_response
+ end
+
+ [kind, fields]
+ end
+
+ def check_spam_action_response!(object)
+ kind, fields = spam_action_response(object)
+
+ case kind
+ when :needs_captcha_response
+ fields.delete :spam
+ raise NeedsCaptchaResponseError.new(NEEDS_CAPTCHA_RESPONSE_MESSAGE, extensions: fields)
+ when :spam
+ raise SpamDisallowedError.new(SPAM_DISALLOWED_MESSAGE, extensions: { spam: true })
+ else
+ nil
+ end
+ end
+ end
+end