summaryrefslogtreecommitdiff
path: root/doc/development/spam_protection_and_captcha/model_and_services.md
blob: 9c5d389a2f545ab481189087b8cb93925d12da62 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
---
stage: Data Science
group: Anti-Abuse
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/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
```