Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 12:08:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 12:08:42 +0300
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/controllers/concerns
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/controllers/concerns')
-rw-r--r--app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb96
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb34
-rw-r--r--app/controllers/concerns/dependency_proxy/auth.rb43
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb6
-rw-r--r--app/controllers/concerns/find_snippet.rb6
-rw-r--r--app/controllers/concerns/integrations_actions.rb3
-rw-r--r--app/controllers/concerns/issuable_actions.rb50
-rw-r--r--app/controllers/concerns/lfs_request.rb15
-rw-r--r--app/controllers/concerns/spammable_actions.rb73
-rw-r--r--app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb28
-rw-r--r--app/controllers/concerns/spammable_actions/attributes.rb13
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/common.rb23
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb35
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb25
14 files changed, 303 insertions, 147 deletions
diff --git a/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb
new file mode 100644
index 00000000000..eebc40f33f4
--- /dev/null
+++ b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module StageActions
+ include Gitlab::Utils::StrongMemoize
+ extend ActiveSupport::Concern
+
+ included do
+ include CycleAnalyticsParams
+
+ before_action :validate_params, only: %i[median]
+ end
+
+ def index
+ result = list_service.execute
+
+ if result.success?
+ render json: cycle_analytics_configuration(result.payload[:stages])
+ else
+ render json: { message: result.message }, status: result.http_status
+ end
+ end
+
+ def median
+ render json: { value: data_collector.median.seconds }
+ end
+
+ def average
+ render json: { value: data_collector.average.seconds }
+ end
+
+ def records
+ serialized_records = data_collector.serialized_records do |relation|
+ add_pagination_headers(relation)
+ end
+
+ render json: serialized_records
+ end
+
+ def count
+ render json: { count: data_collector.count }
+ end
+
+ private
+
+ def parent
+ raise NotImplementedError
+ end
+
+ def value_stream_class
+ raise NotImplementedError
+ end
+
+ def add_pagination_headers(relation)
+ Gitlab::Pagination::OffsetHeaderBuilder.new(
+ request_context: self,
+ per_page: relation.limit_value,
+ page: relation.current_page,
+ next_page: relation.next_page,
+ prev_page: relation.prev_page,
+ params: permitted_cycle_analytics_params
+ ).execute(exclude_total_headers: true, data_without_counts: true)
+ end
+
+ def stage
+ @stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
+ end
+
+ def data_collector
+ @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
+ stage: stage,
+ params: request_params.to_data_collector_params
+ )
+ end
+
+ def value_stream
+ @value_stream ||= value_stream_class.build_default_value_stream(parent)
+ end
+
+ def list_params
+ { value_stream: value_stream }
+ end
+
+ def list_service
+ Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params)
+ end
+
+ def cycle_analytics_configuration(stages)
+ stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
+
+ Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index b74e343f90b..626093b4588 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -16,9 +16,20 @@ module CycleAnalyticsParams
end
def options(params)
- @options ||= { from: start_date(params), current_user: current_user }.merge(date_range(params))
+ @options ||= {}.tap do |opts|
+ opts[:current_user] = current_user
+ opts[:projects] = params[:project_ids] if params[:project_ids]
+ opts[:group] = params[:group_id] if params[:group_id]
+ opts[:from] = params[:from] || start_date(params)
+ opts[:to] = params[:to] if params[:to]
+ opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
+ opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
+ opts.merge!(date_range(params))
+ end
end
+ private
+
def start_date(params)
case params[:start_date]
when '7'
@@ -41,6 +52,27 @@ module CycleAnalyticsParams
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
date.to_time.utc
end
+
+ def permitted_cycle_analytics_params
+ params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION)
+ end
+
+ def all_cycle_analytics_params
+ permitted_cycle_analytics_params.merge(current_user: current_user)
+ end
+
+ def request_params
+ @request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
+ end
+
+ def validate_params
+ if request_params.invalid?
+ render(
+ json: { message: 'Invalid parameters', errors: request_params.errors },
+ status: :unprocessable_entity
+ )
+ end
+ end
end
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
diff --git a/app/controllers/concerns/dependency_proxy/auth.rb b/app/controllers/concerns/dependency_proxy/auth.rb
deleted file mode 100644
index 1276feedba6..00000000000
--- a/app/controllers/concerns/dependency_proxy/auth.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module DependencyProxy
- module Auth
- extend ActiveSupport::Concern
-
- included do
- # We disable `authenticate_user!` since the `DependencyProxy::Auth` performs auth using JWT token
- skip_before_action :authenticate_user!, raise: false
- prepend_before_action :authenticate_user_from_jwt_token!
- end
-
- def authenticate_user_from_jwt_token!
- return unless dependency_proxy_for_private_groups?
-
- authenticate_with_http_token do |token, _|
- user = user_from_token(token)
- sign_in(user) if user
- end
-
- request_bearer_token! unless current_user
- end
-
- private
-
- def dependency_proxy_for_private_groups?
- Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
- end
-
- def request_bearer_token!
- # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
- response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
- render plain: '', status: :unauthorized
- end
-
- def user_from_token(token)
- token_payload = DependencyProxy::AuthTokenService.decoded_token_payload(token)
- User.find(token_payload['user_id'])
- rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
- nil
- end
- end
-end
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
index 2a923d02752..07aca72b22f 100644
--- a/app/controllers/concerns/dependency_proxy/group_access.rb
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -12,15 +12,15 @@ module DependencyProxy
private
def verify_dependency_proxy_enabled!
- render_404 unless group.dependency_proxy_feature_available?
+ render_404 unless group&.dependency_proxy_feature_available?
end
def authorize_read_dependency_proxy!
- access_denied! unless can?(current_user, :read_dependency_proxy, group)
+ access_denied! unless can?(auth_user, :read_dependency_proxy, group)
end
def authorize_admin_dependency_proxy!
- access_denied! unless can?(current_user, :admin_dependency_proxy, group)
+ access_denied! unless can?(auth_user, :admin_dependency_proxy, group)
end
end
end
diff --git a/app/controllers/concerns/find_snippet.rb b/app/controllers/concerns/find_snippet.rb
index d51f1a1b3ad..8a4adbb608f 100644
--- a/app/controllers/concerns/find_snippet.rb
+++ b/app/controllers/concerns/find_snippet.rb
@@ -9,7 +9,7 @@ module FindSnippet
# rubocop:disable CodeReuse/ActiveRecord
def snippet
strong_memoize(:snippet) do
- snippet_klass.inc_relations_for_view.find_by(id: snippet_id)
+ snippet_klass.inc_relations_for_view.find_by(snippet_find_params)
end
end
# rubocop:enable CodeReuse/ActiveRecord
@@ -21,4 +21,8 @@ module FindSnippet
def snippet_id
params[:id]
end
+
+ def snippet_find_params
+ { id: snippet_id }
+ end
end
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index f1fa5c845e2..dd066cc1b02 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -5,8 +5,9 @@ module IntegrationsActions
included do
include Integrations::Params
+ include IntegrationsHelper
- before_action :integration, only: [:edit, :update, :test]
+ before_action :integration, only: [:edit, :update, :overrides, :test]
end
def edit
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 2664a7b7151..7ee680db7f9 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -4,6 +4,9 @@ module IssuableActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include Gitlab::Cache::Helpers
+ include SpammableActions::AkismetMarkAsSpamAction
+ include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
+ include SpammableActions::CaptchaCheck::JsonFormatActionsSupport
included do
before_action :authorize_destroy_issuable!, only: :destroy
@@ -25,17 +28,42 @@ module IssuableActions
end
def update
- @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- respond_to do |format|
- format.html do
- recaptcha_check_if_spammable { render :edit }
+ updated_issuable = update_service.execute(issuable)
+ # NOTE: We only assign the instance variable on this line, and use the local variable
+ # everywhere else in the method, to avoid having to add multiple `rubocop:disable` comments.
+ @issuable = updated_issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ # NOTE: This check for `is_a?(Spammable)` is necessary because not all
+ # possible `issuable` types implement Spammable. Once they all implement Spammable,
+ # this check can be removed.
+ if updated_issuable.is_a?(Spammable)
+ respond_to do |format|
+ format.html do
+ # NOTE: This redirect is intentionally only performed in the case where the updated
+ # issuable is a spammable, and intentionally is not performed in the non-spammable case.
+ # This preserves the legacy behavior of this action.
+ if updated_issuable.valid?
+ redirect_to spammable_path
+ else
+ with_captcha_check_html_format { render :edit }
+ end
+ end
+
+ format.json do
+ with_captcha_check_json_format { render_entity_json }
+ end
end
-
- format.json do
- recaptcha_check_if_spammable(false) { render_entity_json }
+ else
+ respond_to do |format|
+ format.html do
+ render :edit
+ end
+
+ format.json do
+ render_entity_json
+ end
end
end
-
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
@@ -171,12 +199,6 @@ module IssuableActions
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
- def recaptcha_check_if_spammable(should_redirect = true, &block)
- return yield unless issuable.is_a? Spammable
-
- recaptcha_check_with_fallback(should_redirect, &block)
- end
-
def render_conflict_response
respond_to do |format|
format.html do
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 55e0ed8cd42..97df3c7caea 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -4,6 +4,7 @@
# - a `#container` accessor
# - a `#project` accessor
# - a `#user` accessor
+# - a `#deploy_token` accessor
# - a `#authentication_result` accessor
# - a `#can?(object, action, subject)` method
# - a `#ci?` method
@@ -83,26 +84,18 @@ module LfsRequest
end
def deploy_token_can_download_code?
- deploy_token_present? &&
+ deploy_token.present? &&
deploy_token.project == project &&
deploy_token.active? &&
deploy_token.read_repository?
end
- def deploy_token_present?
- user && user.is_a?(DeployToken)
- end
-
- def deploy_token
- user
- end
-
def lfs_upload_access?
strong_memoize(:lfs_upload_access) do
next false unless has_authentication_ability?(:push_code)
next false if limit_exceeded?
- lfs_deploy_token? || can?(user, :push_code, project)
+ lfs_deploy_token? || can?(user, :push_code, project) || can?(deploy_token, :push_code, project)
end
end
@@ -111,7 +104,7 @@ module LfsRequest
end
def user_can_download_code?
- has_authentication_ability?(:download_code) && can?(user, :download_code, project) && !deploy_token_present?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
def build_can_download_code?
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
deleted file mode 100644
index eb1223f22a9..00000000000
--- a/app/controllers/concerns/spammable_actions.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module SpammableActions
- extend ActiveSupport::Concern
- include Spam::Concerns::HasSpamActionResponseFields
-
- included do
- before_action :authorize_submit_spammable!, only: :mark_as_spam
- end
-
- def mark_as_spam
- if Spam::MarkAsSpamService.new(target: spammable).execute
- redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
- else
- redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
- end
- end
-
- private
-
- def recaptcha_check_with_fallback(should_redirect = true, &fallback)
- if should_redirect && spammable.valid?
- redirect_to spammable_path
- elsif spammable.render_recaptcha?
- Gitlab::Recaptcha.load_configurations!
-
- respond_to do |format|
- format.html do
- # NOTE: format.html is still used by issue create, and uses the legacy HAML
- # `_recaptcha_form.html.haml` rendered via the `projects/issues/verify` template.
- render :verify
- end
-
- format.json do
- # format.json is used by all new Vue-based CAPTCHA implementations, which
- # handle all of the CAPTCHA form rendering on the client via the Pajamas-based
- # app/assets/javascripts/captcha/captcha_modal.vue
-
- # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response
- # which requires a CAPTCHA to be solved in order for the request to be resubmitted.
- # See https://stackoverflow.com/q/26547466/25192
- render json: spam_action_response_fields(spammable), status: :conflict
- end
- end
- else
- yield
- end
- end
-
- # TODO: This method is currently only needed for issue create, to convert spam/CAPTCHA values from
- # params, and instead be passed as headers, as the spam services now all expect. It can be removed
- # when issue create is is converted to a client/JS based approach instead of the legacy HAML
- # `_recaptcha_form.html.haml` which is rendered via the `projects/issues/verify` template.
- # In that case, which is based on the legacy reCAPTCHA implementation using the HTML/HAML form,
- # the 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
- # recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
- def extract_legacy_spam_params_to_headers
- request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] || params[:captcha_response]
- request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id]
- end
-
- def spammable
- raise NotImplementedError, "#{self.class} does not implement #{__method__}"
- end
-
- def spammable_path
- raise NotImplementedError, "#{self.class} does not implement #{__method__}"
- end
-
- def authorize_submit_spammable!
- access_denied! unless current_user.admin?
- end
-end
diff --git a/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb
new file mode 100644
index 00000000000..234c591ffb7
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module SpammableActions::AkismetMarkAsSpamAction
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+
+ included do
+ before_action :authorize_submit_spammable!, only: :mark_as_spam
+ end
+
+ def mark_as_spam
+ if Spam::AkismetMarkAsSpamService.new(target: spammable).execute
+ redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
+ else
+ redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
+ end
+ end
+
+ private
+
+ def authorize_submit_spammable!
+ access_denied! unless current_user.can_admin_all_resources?
+ end
+
+ def spammable_path
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/attributes.rb b/app/controllers/concerns/spammable_actions/attributes.rb
new file mode 100644
index 00000000000..d7060e47c07
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/attributes.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SpammableActions
+ module Attributes
+ extend ActiveSupport::Concern
+
+ private
+
+ def spammable
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/common.rb b/app/controllers/concerns/spammable_actions/captcha_check/common.rb
new file mode 100644
index 00000000000..7c047e02a1d
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/common.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module SpammableActions::CaptchaCheck
+ module Common
+ extend ActiveSupport::Concern
+
+ private
+
+ def with_captcha_check_common(captcha_render_lambda:, &block)
+ # If the Spammable indicates that CAPTCHA is not necessary (either due to it not being flagged
+ # as spam, or if spam/captcha is disabled for some reason), then we will go ahead and
+ # yield to the block containing the action's original behavior, then return.
+ return yield unless spammable.render_recaptcha?
+
+ # If we got here, we need to render the CAPTCHA instead of yielding to action's original
+ # behavior. We will present a CAPTCHA to be solved by executing the lambda which was passed
+ # as the `captcha_render_lambda:` argument. This lambda contains either the HTML-specific or
+ # JSON-specific behavior to cause the CAPTCHA modal to be rendered.
+ Gitlab::Recaptcha.load_configurations!
+ captcha_render_lambda.call
+ end
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb
new file mode 100644
index 00000000000..f687c0fcf2d
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# This module should *ONLY* be included if needed to support forms submits with HTML MIME type.
+# In other words, forms handled by actions which use a `respond_to` of `format.html`.
+#
+# If the request is handled by actions via `format.json`, for example, for all Javascript based form
+# submissions and Vue components which use Apollo and Axios, then the corresponding module
+# which supports JSON format should be used instead.
+module SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+ include SpammableActions::CaptchaCheck::Common
+
+ included do
+ before_action :convert_html_spam_params_to_headers, only: [:create, :update]
+ end
+
+ private
+
+ def with_captcha_check_html_format(&block)
+ captcha_render_lambda = -> { render :captcha_check }
+ with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block)
+ end
+
+ # Convert spam/CAPTCHA values from form field params to headers, because all spam-related services
+ # expect these values to be passed as headers.
+ #
+ # The 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
+ # recaptcha gem. This is a field which is automatically included by calling the
+ # `#recaptcha_tags` method within a HAML template's form.
+ def convert_html_spam_params_to_headers
+ request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] if params['g-recaptcha-response']
+ request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id] if params[:spam_log_id]
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb
new file mode 100644
index 00000000000..0bfea05abc7
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# This module should be included to support forms submits with a 'js' or 'json' type of MIME type.
+# In other words, forms handled by actions which use a `respond_to` of `format.js` or `format.json`.
+#
+# For example, for all Javascript based form submissions and Vue components which use Apollo and Axios
+#
+# If the request is handled by actions via `format.html`, then the corresponding module which
+# supports HTML format should be used instead.
+module SpammableActions::CaptchaCheck::JsonFormatActionsSupport
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+ include SpammableActions::CaptchaCheck::Common
+ include Spam::Concerns::HasSpamActionResponseFields
+
+ private
+
+ def with_captcha_check_json_format(&block)
+ # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response
+ # which requires a CAPTCHA to be solved in order for the request to be resubmitted.
+ # https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10
+ captcha_render_lambda = -> { render json: spam_action_response_fields(spammable), status: :conflict }
+ with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block)
+ end
+end