diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/graphql/mutations | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) | |
download | gitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/graphql/mutations')
17 files changed, 222 insertions, 82 deletions
diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb index 32ca6de9b96..ea1502d4b62 100644 --- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb +++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb @@ -8,7 +8,7 @@ module Mutations ADMIN_MESSAGE = 'You must be an admin to use this mutation' - Labkit::Context::KNOWN_KEYS.each do |key| + Gitlab::ApplicationContext::KNOWN_KEYS.each do |key| argument key, GraphQL::STRING_TYPE, required: false, diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb index 87e6bc46937..c6dc85dc07c 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/create.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb @@ -18,7 +18,7 @@ module Mutations argument :api_url, GraphQL::STRING_TYPE, required: true, - description: 'Endpoint at which prometheus can be queried.' + description: 'Endpoint at which Prometheus can be queried.' def resolve(args) project = authorized_find!(args[:project_path]) diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb index 62fb81bca5a..7594766176f 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/update.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb @@ -16,7 +16,7 @@ module Mutations argument :api_url, GraphQL::STRING_TYPE, required: false, - description: "Endpoint at which prometheus can be queried." + description: "Endpoint at which Prometheus can be queried." def resolve(args) integration = authorized_find!(id: args[:id]) diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index ac5ddc5bd4c..1f18a37fcb9 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -2,13 +2,14 @@ module Mutations class BaseMutation < GraphQL::Schema::RelayClassicMutation - prepend Gitlab::Graphql::Authorize::AuthorizeResource + include Gitlab::Graphql::Authorize::AuthorizeResource prepend Gitlab::Graphql::CopyFieldDescription prepend ::Gitlab::Graphql::GlobalIDCompatibility ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' field_class ::Types::BaseField + argument_class ::Types::BaseArgument field :errors, [GraphQL::STRING_TYPE], null: false, @@ -28,11 +29,29 @@ module Mutations end def ready?(**args) - if Gitlab::Database.read_only? - raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE - else - true + raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only? + + true + end + + def load_application_object(argument, lookup_as_type, id, context) + ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e| + Gitlab::ErrorTracking.track_exception(e) + # The default behaviour is to abort processing and return nil for the + # entire mutation field, but not set any top-level errors. We prefer to + # at least say that something went wrong. + raise_resource_not_available_error! end end + + def self.authorized?(object, context) + # we never provide an object to mutations, but we do need to have a user. + context[:current_user].present? && !context[:current_user].blocked? + end + + # See: AuthorizeResource#authorized_resource? + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end end end diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb index 096ac89db1c..f32205643da 100644 --- a/app/graphql/mutations/boards/issues/issue_move_list.rb +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -52,13 +52,10 @@ module Mutations super end - def resolve(board:, **args) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/247861') + def resolve(board:, project_path:, iid:, **args) + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/247861') - raise_resource_not_available_error! unless board - authorize_board!(board) - - issue = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + issue = authorized_find!(project_path: project_path, iid: iid) move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args)) move_issue(board, issue, move_params) @@ -84,12 +81,6 @@ module Mutations def move_arguments(args) args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id) end - - def authorize_board!(board) - return if Ability.allowed?(current_user, :read_issue_board, board.resource_parent) - - raise_resource_not_available_error! - end end end end diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb index 6b7750ee860..d7451babaea 100644 --- a/app/graphql/mutations/ci/ci_cd_settings_update.rb +++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb @@ -17,13 +17,23 @@ module Mutations required: false, description: 'Indicates if the latest artifact should be kept for this project.' + field :ci_cd_settings, + Types::Ci::CiCdSettingType, + null: false, + description: 'The CI/CD settings after mutation.' + def resolve(full_path:, **args) project = authorized_find!(full_path) settings = project.ci_cd_settings settings.update(args) - { errors: errors_on_object(settings) } + { + ci_cd_settings: settings, + errors: errors_on_object(settings) + } end end end end + +Mutations::Ci::CiCdSettingsUpdate.prepend_if_ee('::EE::Mutations::Ci::CiCdSettingsUpdate') diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb index f6f4b744f4e..d3ab0a1779a 100644 --- a/app/graphql/mutations/concerns/mutations/assignable.rb +++ b/app/graphql/mutations/concerns/mutations/assignable.rb @@ -13,19 +13,15 @@ module Mutations argument :operation_mode, Types::MutationOperationModeEnum, required: false, + default_value: Types::MutationOperationModeEnum.default_mode, description: 'The operation to perform. Defaults to REPLACE.' end - def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace]) + def resolve(project_path:, iid:, assignee_usernames:, operation_mode:) resource = authorized_find!(project_path: project_path, iid: iid) + users = new_assignees(resource, assignee_usernames) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098') if resource.is_a?(MergeRequest) - - update_service_class.new( - resource.project, - current_user, - assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode) - ).execute(resource) + assign!(resource, users, operation_mode) { resource.class.name.underscore.to_sym => resource, @@ -35,18 +31,32 @@ module Mutations private - def assignee_ids(resource, usernames, operation_mode) - assignee_ids = [] - assignee_ids += resource.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode) - user_ids = UsersFinder.new(current_user, username: usernames).execute.map(&:id) + def assign!(resource, users, operation_mode) + update_service_class.new( + resource.project, + current_user, + assignee_ids: assignee_ids(resource, users, operation_mode) + ).execute(resource) + end - if operation_mode == Types::MutationOperationModeEnum.enum[:remove] - assignee_ids -= user_ids - else - assignee_ids |= user_ids - end + def new_assignees(resource, usernames) + UsersFinder.new(current_user, username: usernames).execute.to_a + end + + def assignee_ids(resource, users, mode) + transform_list(mode, resource, users.map(&:id)) + end + + def current_assignee_ids(resource) + resource.assignees.map(&:id) + end - assignee_ids + def transform_list(mode, resource, new_values) + case mode + when 'REPLACE' then new_values + when 'APPEND' then current_assignee_ids(resource) | new_values + when 'REMOVE' then current_assignee_ids(resource) - new_values + end end end end diff --git a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb index ba644eff36c..3c5f077110c 100644 --- a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb +++ b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb @@ -1,64 +1,51 @@ # frozen_string_literal: true module Mutations - # This concern can be mixed into a mutation to provide support for spam checking, - # and optionally support the workflow to allow clients to display and solve CAPTCHAs. + # This concern is deprecated and will be deleted in 14.6 + # + # Use the SpamProtection concern instead. module CanMutateSpammable extend ActiveSupport::Concern - include Spam::Concerns::HasSpamActionResponseFields - # NOTE: The arguments and fields are intentionally named with 'captcha' instead of 'recaptcha', - # so that they can be applied to future alternative CAPTCHA implementations other than - # reCAPTCHA (e.g. FriendlyCaptcha) without having to change the names and descriptions in the API. + DEPRECATION_NOTICE = { + reason: 'Use spam protection with HTTP headers instead', + milestone: '13.11' + }.freeze + included do argument :captcha_response, GraphQL::STRING_TYPE, required: false, + deprecated: DEPRECATION_NOTICE, description: 'A valid CAPTCHA response value obtained by using the provided captchaSiteKey with a CAPTCHA API to present a challenge to be solved on the client. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".' argument :spam_log_id, GraphQL::INT_TYPE, required: false, + deprecated: DEPRECATION_NOTICE, description: 'The spam log ID which must be passed along with a valid CAPTCHA response for the operation to be completed. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".' field :spam, GraphQL::BOOLEAN_TYPE, null: true, + deprecated: DEPRECATION_NOTICE, description: 'Indicates whether the operation was detected as definite spam. There is no option to resubmit the request with a CAPTCHA response.' field :needs_captcha_response, GraphQL::BOOLEAN_TYPE, null: true, + deprecated: DEPRECATION_NOTICE, description: 'Indicates whether the operation was detected as possible spam and not completed. If CAPTCHA is enabled, the request must be resubmitted with a valid CAPTCHA response and spam_log_id included for the operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.' field :spam_log_id, GraphQL::INT_TYPE, null: true, + deprecated: DEPRECATION_NOTICE, description: 'The spam log ID which must be passed along with a valid CAPTCHA response for an operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.' field :captcha_site_key, GraphQL::STRING_TYPE, null: true, + deprecated: DEPRECATION_NOTICE, description: 'The CAPTCHA site key which must be used to render a challenge for the user to solve to obtain a valid captchaResponse value. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.' end - - private - - # additional_spam_params -> hash - # - # Used from a spammable mutation's #resolve method to generate - # the required additional spam/recaptcha params which must be merged into the params - # passed to the constructor of a service, where they can then be used in the service - # to perform spam checking via SpamActionService. - # - # Also accesses the #context of the mutation's Resolver superclass to obtain the request. - # - # Example: - # - # existing_args.merge!(additional_spam_params) - def additional_spam_params - { - api: true, - request: context[:request] - } - end end end diff --git a/app/graphql/mutations/concerns/mutations/spam_protection.rb b/app/graphql/mutations/concerns/mutations/spam_protection.rb new file mode 100644 index 00000000000..d765da23a4b --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/spam_protection.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Mutations + # This concern can be mixed into a mutation to provide support for spam checking, + # and optionally support the workflow to allow clients to display and solve CAPTCHAs. + module SpamProtection + extend ActiveSupport::Concern + include Spam::Concerns::HasSpamActionResponseFields + + SpamActionError = Class.new(GraphQL::ExecutionError) + NeedsCaptchaResponseError = Class.new(SpamActionError) + SpamDisallowedError = Class.new(SpamActionError) + + NEEDS_CAPTCHA_RESPONSE_MESSAGE = "Request denied. Solve CAPTCHA challenge and retry" + SPAM_DISALLOWED_MESSAGE = "Request denied. Spam detected" + + private + + # additional_spam_params -> hash + # + # Used from a spammable mutation's #resolve method to generate + # the required additional spam/CAPTCHA params which must be merged into the params + # passed to the constructor of a service, where they can then be used in the service + # to perform spam checking via SpamActionService. + # + # Also accesses the #context of the mutation's Resolver superclass to obtain the request. + # + # Example: + # + # existing_args.merge!(additional_spam_params) + def additional_spam_params + { + api: true, + request: context[:request] + } + end + + def spam_action_response(object) + fields = spam_action_response_fields(object) + + # If the SpamActionService detected something as spam, + # this is non-recoverable and the needs_captcha_response + # should not be considered + kind = if fields[:spam] + :spam + elsif fields[:needs_captcha_response] + :needs_captcha_response + end + + [kind, fields] + end + + def check_spam_action_response!(object) + kind, fields = spam_action_response(object) + + case kind + when :needs_captcha_response + fields.delete :spam + raise NeedsCaptchaResponseError.new(NEEDS_CAPTCHA_RESPONSE_MESSAGE, extensions: fields) + when :spam + raise SpamDisallowedError.new(SPAM_DISALLOWED_MESSAGE, extensions: { spam: true }) + else + nil + end + end + end +end diff --git a/app/graphql/mutations/container_repositories/destroy_tags.rb b/app/graphql/mutations/container_repositories/destroy_tags.rb index 636ceccee04..12d65f604b8 100644 --- a/app/graphql/mutations/container_repositories/destroy_tags.rb +++ b/app/graphql/mutations/container_repositories/destroy_tags.rb @@ -3,7 +3,7 @@ module Mutations module ContainerRepositories class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase - LIMIT = 20.freeze + LIMIT = 20 TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}" diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb index 3f97325c921..0f2af99bf61 100644 --- a/app/graphql/mutations/issues/move.rb +++ b/app/graphql/mutations/issues/move.rb @@ -11,7 +11,7 @@ module Mutations description: 'The project to move the issue to.' def resolve(project_path:, iid:, target_project_path:) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/267762') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20816') issue = authorized_find!(project_path: project_path, iid: iid) source_project = issue.project diff --git a/app/graphql/mutations/issues/set_assignees.rb b/app/graphql/mutations/issues/set_assignees.rb index a4d1c755b53..8413c89b010 100644 --- a/app/graphql/mutations/issues/set_assignees.rb +++ b/app/graphql/mutations/issues/set_assignees.rb @@ -7,6 +7,19 @@ module Mutations include Assignable + def assign!(issue, users, mode) + permitted, forbidden = users.partition { |u| u.can?(:read_issue, issue) } + + super(issue, permitted, mode) + + forbidden.each do |user| + issue.errors.add( + :assignees, + "Cannot assign #{user.to_reference} to #{issue.to_reference}" + ) + end + end + def update_service_class ::Issues::UpdateService end diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb index 540be7098ac..da94dcd8890 100644 --- a/app/graphql/mutations/merge_requests/accept.rb +++ b/app/graphql/mutations/merge_requests/accept.rb @@ -42,7 +42,8 @@ module Mutations description: 'Squash commits on the source branch before merge.' def resolve(project_path:, iid:, **args) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42317') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796') + merge_request = authorized_find!(project_path: project_path, iid: iid) project = merge_request.target_project merge_params = args.compact.with_indifferent_access diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb index 548c6b55a85..dc96523685e 100644 --- a/app/graphql/mutations/merge_requests/set_assignees.rb +++ b/app/graphql/mutations/merge_requests/set_assignees.rb @@ -8,7 +8,7 @@ module Mutations include Assignable def update_service_class - ::MergeRequests::UpdateService + ::MergeRequests::UpdateAssigneesService end end end diff --git a/app/graphql/mutations/release_asset_links/delete.rb b/app/graphql/mutations/release_asset_links/delete.rb new file mode 100644 index 00000000000..dd450f36cdd --- /dev/null +++ b/app/graphql/mutations/release_asset_links/delete.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module ReleaseAssetLinks + class Delete < BaseMutation + graphql_name 'ReleaseAssetLinkDelete' + + authorize :destroy_release + + ReleaseAssetLinkID = ::Types::GlobalIDType[::Releases::Link] + + argument :id, ReleaseAssetLinkID, + required: true, + description: 'ID of the release asset link to delete.' + + field :link, + Types::ReleaseAssetLinkType, + null: true, + description: 'The deleted release asset link.' + + def resolve(id:) + link = authorized_find!(id) + + unless link.destroy + return { link: nil, errors: link.errors.full_messages } + end + + { link: link, errors: [] } + end + + def find_object(id) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ReleaseAssetLinkID.coerce_isolated_input(id) + + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index 7f2dd448b8b..e9b45294659 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -5,6 +5,7 @@ module Mutations class Create < BaseMutation include ServiceCompatibility include CanMutateSpammable + include Mutations::SpamProtection authorize :create_snippet @@ -56,12 +57,12 @@ module Mutations end snippet = service_response.payload[:snippet] - with_spam_action_response_fields(snippet) do - { - snippet: service_response.success? ? snippet : nil, - errors: errors_on_object(snippet) - } - end + check_spam_action_response!(snippet) + + { + snippet: service_response.success? ? snippet : nil, + errors: errors_on_object(snippet) + } end private diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index 9f9f8bca848..b9b9b13eebb 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -5,6 +5,7 @@ module Mutations class Update < Base include ServiceCompatibility include CanMutateSpammable + include Mutations::SpamProtection graphql_name 'UpdateSnippet' @@ -45,12 +46,12 @@ module Mutations end snippet = service_response.payload[:snippet] - with_spam_action_response_fields(snippet) do - { - snippet: service_response.success? ? snippet : snippet.reset, - errors: errors_on_object(snippet) - } - end + check_spam_action_response!(snippet) + + { + snippet: service_response.success? ? snippet : snippet.reset, + errors: errors_on_object(snippet) + } end private |