diff options
Diffstat (limited to 'app/controllers/concerns')
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 |