debian-mirror-gitlab/doc/development/spam_protection_and_captcha/web_ui.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

197 lines
8.7 KiB
Markdown
Raw Normal View History

2022-05-07 20:08:51 +05:30
---
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
---
# Web UI spam protection and CAPTCHA support
The approach for adding spam protection and CAPTCHA support to a new UI area of the GitLab application
depends upon how the existing code is implemented.
## Supported scenarios of request submissions
Three different scenarios are supported. Two are used with JavaScript XHR/Fetch requests
for either Apollo or Axios, and one is used only with standard HTML form requests:
1. A JavaScript-based submission (possibly via Vue)
1. Using Apollo (GraphQL API via Fetch/XHR request)
1. Using Axios (REST API via Fetch/XHR request)
1. A standard HTML form submission (HTML request)
Some parts of the implementation depend upon which of these scenarios you must support.
## Implementation tasks specific to JavaScript XHR/Fetch requests
Two approaches are fully supported:
1. Apollo, using the GraphQL API.
1. Axios, using either the GraphQL API.
The spam and CAPTCHA-related data communication between the frontend and backend requires no
additional fields being added to the models. Instead, communication is handled:
- Through custom header values in the request.
- Through top-level JSON fields in the response.
The spam and CAPTCHA-related logic is also cleanly abstracted into reusable modules and helper methods
which can wrap existing logic, and only alter the existing flow if potential spam
is detected or a CAPTCHA display is needed. This approach allows the spam and CAPTCHA
2022-06-21 17:19:12 +05:30
support to be added to new areas of the application with minimal changes to
2022-05-07 20:08:51 +05:30
existing logic. In the case of the frontend, potentially **zero** changes are needed!
On the frontend, this is handled abstractly and transparently using `ApolloLink` for Apollo, and an
Axios interceptor for Axios. The CAPTCHA display is handled by a standard GitLab UI / Pajamas modal
component. You can find all the relevant frontend code under `app/assets/javascripts/captcha`.
However, even though the actual handling of the request interception and
modal is transparent, without any mandatory changes to the involved JavaScript or Vue components
for the form or page, changes in request or error handling may be required. Changes are needed
because the existing behavior may not work correctly: for example, if a failed or cancelled
CAPTCHA display interrupts the normal request flow or UI updates.
Careful exploratory testing of all scenarios is important to uncover any potential
problems.
This sequence diagram illustrates the normal CAPTCHA flow for JavaScript XHR/Fetch requests
on the frontend:
```mermaid
sequenceDiagram
participant U as User
participant V as Vue/JS Application
participant A as ApolloLink or Axios Interceptor
participant G as GitLab API
U->>V: Save model
V->>A: Request
A->>G: Request
G--xA: Response with error and spam/CAPTCHA related fields
A->>U: CAPTCHA presented in modal
U->>A: CAPTCHA solved to obtain valid CAPTCHA response
A->>G: Request with valid CAPTCHA response and SpamLog ID in headers
G-->>A: Response with success
A-->>V: Response with success
```
The backend is also cleanly abstracted via mixin modules and helper methods. The three main
changes required to the relevant backend controller actions (normally just `create`/`update`) are:
2022-06-21 17:19:12 +05:30
1. Create a `SpamParams` parameter object instance based on the request, using the static
2022-05-07 20:08:51 +05:30
`#new_from_request` factory method. This method takes a request, and returns a `SpamParams` instance.
1. Pass the created `SpamParams` instance as the `spam_params` named argument to the
Service class constructor, which you should have already added. If the spam check indicates
the changes to the model are possibly spam, then:
- An error is added to the model.
- The `needs_recaptcha` property on the model is set to true.
1. Wrap the existing controller action return value (rendering or redirecting) in a block passed to
a `#with_captcha_check_json_format` helper method, which transparently handles:
1. Check if CAPTCHA is enabled, and if so, proceeding with the next step.
1. Checking if there the model contains an error, and the `needs_recaptcha` flag is true.
- If yes: Add the appropriate spam or CAPTCHA fields to the JSON response, and return
a `409 - Conflict` HTTP status code.
- If no (if CAPTCHA is disabled or if no spam was detected): The normal request return
logic passed in the block is run.
Thanks to the abstractions, it's more straightforward to implement than it is to explain it.
You don't have to worry much about the hidden details!
Make these changes:
## Add support to the controller actions
If the feature's frontend submits directly to controller actions, and does not only use the GraphQL
API, then you must add support to the appropriate controllers.
The action methods may be directly in the controller class, or they may be abstracted
to a module included in the controller class. Our example uses a module. The
only difference when directly modifying the controller:
`extend ActiveSupport::Concern` is not required.
```ruby
module WidgetsActions
# NOTE: This `extend` probably already exists, but it MUST be moved to occur BEFORE all
# `include` statements. Otherwise, confusing bugs may occur in which the methods
# in the included modules cannot be found.
extend ActiveSupport::Concern
include SpammableActions::CaptchaCheck::JsonFormatActionsSupport
def create
spam_params = ::Spam::SpamParams.new_from_request(request: request)
widget = ::Widgets::CreateService.new(
project: project,
current_user: current_user,
params: params,
spam_params: spam_params
).execute
respond_to do |format|
format.json do
with_captcha_check_json_format do
# The action's existing `render json: ...` (or wrapper method) and related logic. Possibly
# including different rendering cases if the model is valid or not. It's all wrapped here
# within the `with_captcha_check_json_format` block. For example:
if widget.valid?
render json: serializer.represent(widget)
else
render json: { errors: widget.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
end
end
```
## Implementation tasks specific to HTML form requests
Some areas of the application have not been converted to use the GraphQL API via
a JavaScript client, but instead rely on standard Rails HAML form submissions via an
`HTML` MIME type request. In these areas, the action returns a pre-rendered HTML (HAML) page
as the response body. Unfortunately, in this case
[it is not possible](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66427#note_636989204)
to use any of the JavaScript-based frontend support as described above. Instead we must use an
alternate approach which handles the rendering of the CAPTCHA form via a HAML template.
Everything is still cleanly abstracted, and the implementation in the backend
controllers is virtually identical to the JavaScript/JSON based approach. Replace the
word `JSON` with `HTML` (using the appropriate case) in the module names and helper methods.
The action methods might be directly in the controller, or they
might be in a module. In this example, they are directly in the
controller, and we also do an `update` method instead of `create`:
```ruby
class WidgetsController < ApplicationController
include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
def update
# Existing logic to find the `widget` model instance...
spam_params = ::Spam::SpamParams.new_from_request(request: request)
::Widgets::UpdateService.new(
project: project,
current_user: current_user,
params: params,
spam_params: spam_params
).execute(widget)
respond_to do |format|
format.html do
if widget.valid?
# NOTE: `spammable_path` is required by the `SpammableActions::AkismetMarkAsSpamAction`
# module, and it should have already been implemented on this controller according to
# the instructions above. It is reused here to avoid duplicating the route helper call.
redirect_to spammable_path
else
# If we got here, there were errors on the model instance - from a failed spam check
# and/or other validation errors on the model. Either way, we'll re-render the form,
# and if a CAPTCHA render is necessary, it will be automatically handled by
# `with_captcha_check_html_format`
with_captcha_check_html_format { render :edit }
end
end
end
end
end
```