diff options
Diffstat (limited to 'doc/development/spam_protection_and_captcha/web_ui.md')
-rw-r--r-- | doc/development/spam_protection_and_captcha/web_ui.md | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/doc/development/spam_protection_and_captcha/web_ui.md b/doc/development/spam_protection_and_captcha/web_ui.md new file mode 100644 index 00000000000..6aa01f401bd --- /dev/null +++ b/doc/development/spam_protection_and_captcha/web_ui.md @@ -0,0 +1,196 @@ +--- +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 easily 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 simple 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 +``` |