summaryrefslogtreecommitdiff
path: root/doc/development/spam_protection_and_captcha/model_and_services.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development/spam_protection_and_captcha/model_and_services.md')
-rw-r--r--doc/development/spam_protection_and_captcha/model_and_services.md155
1 files changed, 155 insertions, 0 deletions
diff --git a/doc/development/spam_protection_and_captcha/model_and_services.md b/doc/development/spam_protection_and_captcha/model_and_services.md
new file mode 100644
index 00000000000..d0519cba68f
--- /dev/null
+++ b/doc/development/spam_protection_and_captcha/model_and_services.md
@@ -0,0 +1,155 @@
+---
+stage: Manage
+group: Authentication and Authorization
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Model and services spam protection and CAPTCHA support
+
+Before adding any spam or CAPTCHA support to the REST API, GraphQL API, or Web UI, you must
+first add the necessary support to:
+
+1. The backend ActiveRecord models.
+1. The services layer.
+
+All or most of the following changes are required, regardless of the type of spam or CAPTCHA request
+implementation you are supporting. Some newer features which are completely based on the GraphQL API
+may not have any controllers, and don't require you to add the `mark_as_spam` action to the controller.
+
+To do this:
+
+1. [Add `Spammable` support to the ActiveRecord model](#add-spammable-support-to-the-activerecord-model).
+1. [Add support for the `mark_as_spam` action to the controller](#add-support-for-the-mark_as_spam-action-to-the-controller).
+1. [Add a call to SpamActionService to the execute method of services](#add-a-call-to-spamactionservice-to-the-execute-method-of-services).
+
+## Add `Spammable` support to the ActiveRecord model
+
+1. Include the `Spammable` module in the model class:
+
+ ```ruby
+ include Spammable
+ ```
+
+1. Add: `attr_spammable` to indicate which fields can be checked for spam. Up to
+ two fields per model are supported: a "`title`" and a "`description`". You can
+ designate which fields to consider the "`title`" or "`description`". For example,
+ this line designates the `content` field as the `description`:
+
+ ```ruby
+ attr_spammable :content, spam_description: true
+ ```
+
+1. Add a `#check_for_spam?` method implementation:
+
+ ```ruby
+ def check_for_spam?(user:)
+ # Return a boolean result based on various applicable checks, which may include
+ # which attributes have changed, the type of user, whether the data is publicly
+ # visible, and other criteria. This may vary based on the type of model, and
+ # may change over time as spam checking requirements evolve.
+ end
+ ```
+
+ Refer to other existing `Spammable` models'
+ implementations of this method for examples of the required logic checks.
+
+## Add support for the `mark_as_spam` action to the controller
+
+The `SpammableActions::AkismetMarkAsSpamAction` module adds support for a `#mark_as_spam` action
+to a controller. This controller allows administrators to manage spam for the associated
+`Spammable` model in the [Spam Log section](../../integration/akismet.md) of the Admin Area page.
+
+1. Include the `SpammableActions::AkismetMarkAsSpamAction` module in the controller.
+
+ ```ruby
+ include SpammableActions::AkismetMarkAsSpamAction
+ ```
+
+1. Add a `#spammable_path` method implementation. The spam administration page redirects
+ to this page after edits. Refer to other existing controllers' implementations
+ of this method for examples of the type of path logic required. In general, it should
+ be the `#show` action for the `Spammable` model's controller.
+
+ ```ruby
+ def spammable_path
+ widget_path(widget)
+ end
+ ```
+
+NOTE:
+There may be other changes needed to controllers, depending on how the feature is
+implemented. See [Web UI](web_ui.md) for more details.
+
+## Add a call to SpamActionService to the execute method of services
+
+This approach applies to any service which can persist spammable attributes:
+
+1. In the relevant Create or Update service under `app/services`, pass in a populated
+ `Spam::SpamParams` instance. (Refer to instructions later on in this page.)
+1. Use it and the `Spammable` model instance to execute a `Spam::SpamActionService` instance.
+1. If the spam check fails:
+ - An error is added to the model, which causes it to be invalid and prevents it from being saved.
+ - The `needs_recaptcha` property is set to `true`.
+
+ These changes to the model enable it for handling by the subsequent backend and frontend CAPTCHA logic.
+
+Make these changes to each relevant service:
+
+1. Change the constructor to take a `spam_params:` argument as a required named argument.
+
+ Using named arguments for the constructor helps you identify all the calls to
+ the constructor that need changing. It's less risky because the interpreter raises
+ type errors unless the caller is changed to pass the `spam_params` argument.
+ If you use an IDE (such as RubyMine) which supports this, your
+ IDE flags it as an error in the editor.
+
+1. In the constructor, set the `@spam_params` instance variable from the `spam_params` constructor
+ argument. Add an `attr_reader: :spam_params` in the `private` section of the class.
+
+1. In the `execute` method, add a call to execute the `Spam::SpamActionService`.
+ (You can also use `before_create` or `before_update`, if the service
+ uses that pattern.) This method uses named arguments, so its usage is clear if
+ you refer to existing examples. However, two important considerations exist:
+ 1. The `SpamActionService` must be executed _after_ all necessary changes are made to
+ the unsaved (and dirty) `Spammable` model instance. This ordering ensures
+ spammable attributes exist to be spam-checked.
+ 1. The `SpamActionService` must be executed _before_ the model is checked for errors and
+ attempting a `save`. If potential spam is detected in the model's changed attributes, we must prevent a save.
+
+```ruby
+module Widget
+ class CreateService < ::Widget::BaseService
+ # NOTE: We require the spam_params and do not default it to nil, because
+ # spam_checking is likely to be necessary. However, if there is not a request available in scope
+ # in the caller (for example, a note 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:)
+ super(project: project, current_user: current_user, params: params)
+
+ @spam_params = spam_params
+ end
+
+ def execute
+ widget = Widget::BuildService.new(project, current_user, params).execute
+
+ # More code that may manipulate dirty model before it is spam checked.
+
+ # NOTE: do this AFTER the spammable model is instantiated, but BEFORE
+ # it is validated or saved.
+ Spam::SpamActionService.new(
+ spammable: widget,
+ spam_params: spam_params,
+ user: current_user,
+ # Or `action: :update` for a UpdateService or service for an existing model.
+ action: :create
+ ).execute
+
+ # Possibly more code related to saving model, but should not change any attributes.
+
+ widget.save
+ end
+
+ private
+
+ attr_reader :spam_params
+```