196 lines
8.7 KiB
Markdown
196 lines
8.7 KiB
Markdown
---
|
|
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
|
|
support to be added to new areas of the application with minimal changes to
|
|
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:
|
|
|
|
1. Create a `SpamParams` parameter object instance based on the request, using the static
|
|
`#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
|
|
```
|