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 | |
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')
115 files changed, 1932 insertions, 398 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 7ab5dc36e4a..8369d0e120f 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -12,7 +12,6 @@ class GitlabSchema < GraphQL::Schema use GraphQL::Pagination::Connections use BatchLoader::GraphQL - use Gitlab::Graphql::Authorize use Gitlab::Graphql::Pagination::Connections use Gitlab::Graphql::GenericTracing use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout @@ -32,9 +31,10 @@ class GitlabSchema < GraphQL::Schema class << self def multiplex(queries, **kwargs) - kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) + kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless kwargs.key?(:max_complexity) queries.each do |query| + query[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless query.key?(:max_complexity) query[:max_depth] = max_query_depth(kwargs[:context]) end @@ -111,6 +111,7 @@ class GitlabSchema < GraphQL::Schema # # Options: # * :expected_type [Class] - the type of object this GlobalID should refer to. + # * :expected_type [[Class]] - array of the types of object this GlobalID should refer to. # # e.g. # @@ -120,14 +121,14 @@ class GitlabSchema < GraphQL::Schema # gid.model_class == ::Project # ``` def parse_gid(global_id, ctx = {}) - expected_type = ctx[:expected_type] + expected_types = Array(ctx[:expected_type]) gid = GlobalID.parse(global_id) raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid - if expected_type && !gid.model_class.ancestors.include?(expected_type) - vars = { global_id: global_id, expected_type: expected_type } - msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars + if expected_types.any? && expected_types.none? { |type| gid.model_class.ancestors.include?(type) } + vars = { global_id: global_id, expected_types: expected_types.join(', ') } + msg = _('%{global_id} is not a valid ID for %{expected_types}.') % vars raise Gitlab::Graphql::Errors::ArgumentError, msg end 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 diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql index 92323923266..959bf7dc91d 100644 --- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql +++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql @@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { __typename id iid + usesNeeds downstream { __typename nodes { diff --git a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb index 94a72bca7c7..abc54614a59 100644 --- a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb +++ b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb @@ -3,19 +3,39 @@ module Resolvers module AlertManagement class HttpIntegrationsResolver < BaseResolver - alias_method :project, :synchronized_object + include ::Gitlab::Graphql::Laziness + + alias_method :project, :object + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: false, + description: 'ID of the integration.' type Types::AlertManagement::HttpIntegrationType.connection_type, null: true - def resolve(**args) - http_integrations + def resolve(id: nil) + return [] unless Ability.allowed?(current_user, :admin_operations, project) + + if id + integrations_by(gid: id) + else + http_integrations + end end private - def http_integrations - return [] unless Ability.allowed?(current_user, :admin_operations, project) + def integrations_by(gid:) + id = Types::GlobalIDType[::AlertManagement::HttpIntegration].coerce_isolated_input(gid) + object = GitlabSchema.find_by_gid(id) + + defer { object }.then do |integration| + ret = integration if project == integration&.project + Array.wrap(ret) + end + end + def http_integrations ::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute end end diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb index 4d1fe367277..cb7e73c2d1a 100644 --- a/app/graphql/resolvers/alert_management/integrations_resolver.rb +++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb @@ -3,27 +3,60 @@ module Resolvers module AlertManagement class IntegrationsResolver < BaseResolver - alias_method :project, :synchronized_object + include ::Gitlab::Graphql::Laziness + + alias_method :project, :object + + argument :id, ::Types::GlobalIDType, + required: false, + description: 'ID of the integration.' type Types::AlertManagement::IntegrationType.connection_type, null: true - def resolve(**args) - http_integrations + prometheus_integrations + def resolve(id: nil) + if id + integrations_by(gid: id) + else + http_integrations + prometheus_integrations + end end private + def integrations_by(gid:) + object = GitlabSchema.object_from_id(gid, expected_type: expected_integration_types) + defer { object }.then do |integration| + ret = integration if project == integration&.project + Array.wrap(ret) + end + end + def prometheus_integrations - return [] unless Ability.allowed?(current_user, :admin_project, project) + return [] unless prometheus_integrations_allowed? Array(project.prometheus_service) end def http_integrations - return [] unless Ability.allowed?(current_user, :admin_operations, project) + return [] unless http_integrations_allowed? ::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute end + + def prometheus_integrations_allowed? + Ability.allowed?(current_user, :admin_project, project) + end + + def http_integrations_allowed? + Ability.allowed?(current_user, :admin_operations, project) + end + + def expected_integration_types + [].tap do |types| + types << ::AlertManagement::HttpIntegration if http_integrations_allowed? + types << ::PrometheusService if prometheus_integrations_allowed? + end + end end end end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 67bba079512..48563633d11 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -39,9 +39,7 @@ module Resolvers as_single << block # Have we been called after defining the single version of this resolver? - if @single.present? - @single.instance_exec(&block) - end + @single.instance_exec(&block) if @single.present? end def self.as_single @@ -90,7 +88,7 @@ module Resolvers def self.last parent = self - @last ||= Class.new(self.single) do + @last ||= Class.new(single) do type parent.singular_type, null: true def select_result(results) @@ -138,16 +136,6 @@ module Resolvers end end - # TODO: remove! This should never be necessary - # Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/13984, - # since once we use that authorization approach, the object is guaranteed to - # be synchronized before any field. - def synchronized_object - strong_memoize(:synchronized_object) do - ::Gitlab::Graphql::Lazy.force(object) - end - end - def single? false end @@ -160,5 +148,13 @@ module Resolvers def select_result(results) results end + + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(try(:required_permissions)) + end + + def self.authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end end end diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb new file mode 100644 index 00000000000..d006769bd4b --- /dev/null +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Resolvers + class BlobsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::Tree::BlobType.connection_type, null: true + authorize :download_code + calls_gitaly! + + alias_method :repository, :object + + argument :paths, [GraphQL::STRING_TYPE], + required: true, + description: 'Array of desired blob paths.' + argument :ref, GraphQL::STRING_TYPE, + required: false, + default_value: nil, + description: 'The commit ref to get the blobs from. Default value is HEAD.' + + # We fetch blobs from Gitaly efficiently but it still scales O(N) with the + # number of paths being fetched, so apply a scaling limit to that. + def self.resolver_complexity(args, child_complexity:) + super + (args[:paths] || []).size + end + + def resolve(paths:, ref:) + authorize!(repository.container) + + return [] if repository.empty? + + ref ||= repository.root_ref + + repository.blobs_at(paths.map { |path| [ref, path] }) + end + end +end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index e66f7b97b40..0b699006626 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -3,13 +3,12 @@ module Resolvers class BoardListsResolver < BaseResolver include BoardIssueFilterable - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead type Types::BoardListType, null: true - extras [:lookahead] - authorize :read_issue_board_list + authorizes_object! argument :id, Types::GlobalIDType[List], required: false, @@ -21,15 +20,11 @@ module Resolvers alias_method :board, :object - def resolve(lookahead: nil, id: nil, issue_filters: {}) - authorize!(board) - + def resolve_with_lookahead(id: nil, issue_filters: {}) lists = board_lists(id) context.scoped_set!(:issue_filters, issue_filters(issue_filters)) - if load_preferences?(lookahead) - List.preload_preferences_for_user(lists, current_user) - end + List.preload_preferences_for_user(lists, current_user) if load_preferences? offset_pagination(lists) end @@ -46,9 +41,8 @@ module Resolvers service.execute(board, create_default_lists: false) end - def load_preferences?(lookahead) - lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) || - lookahead&.selection(:nodes)&.selects?(:collapsed) + def load_preferences? + node_selection&.selects?(:collapsed) end def extract_list_id(gid) diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb index 637d690e4cd..85362ab1422 100644 --- a/app/graphql/resolvers/board_resolver.rb +++ b/app/graphql/resolvers/board_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class BoardResolver < BaseResolver.single - alias_method :parent, :synchronized_object + alias_method :parent, :object type Types::BoardType, null: true diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb index f8670649e48..252c9d3acf0 100644 --- a/app/graphql/resolvers/ci/config_resolver.rb +++ b/app/graphql/resolvers/ci/config_resolver.rb @@ -7,6 +7,10 @@ module Resolvers include ResolvesProject type Types::Ci::Config::ConfigType, null: true + description <<~MD + Linted and processed contents of a CI config. + Should not be requested more than once per request. + MD authorize :read_pipeline @@ -55,7 +59,7 @@ module Resolvers name: job[:name], stage: job[:stage], group_name: CommitStatus.new(name: job[:name]).group_name, - needs: job.dig(:needs) || [], + needs: job[:needs] || [], allow_failure: job[:allow_failure], before_script: job[:before_script], script: job[:script], diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb index dd565094017..5ae9e721cc8 100644 --- a/app/graphql/resolvers/ci/jobs_resolver.rb +++ b/app/graphql/resolvers/ci/jobs_resolver.rb @@ -11,7 +11,18 @@ module Resolvers required: false, description: 'Filter jobs by the type of security report they produce.' - def resolve(security_report_types: []) + argument :statuses, [::Types::Ci::JobStatusEnum], + required: false, + description: 'Filter jobs by status.' + + def resolve(statuses: nil, security_report_types: []) + jobs = init_collection(security_report_types) + jobs = jobs.with_status(statuses) if statuses.present? + + jobs + end + + def init_collection(security_report_types) if security_report_types.present? ::Security::SecurityJobsFinder.new( pipeline: pipeline, diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb index 98170e0cd2e..a458e873935 100644 --- a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb +++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb @@ -16,7 +16,7 @@ module Resolvers def preloads { - statuses: [:needs] + jobs: { latest_statuses: [:needs] } } end end diff --git a/app/graphql/resolvers/ci/runner_platforms_resolver.rb b/app/graphql/resolvers/ci/runner_platforms_resolver.rb index 9677c5139b4..f120e94b67b 100644 --- a/app/graphql/resolvers/ci/runner_platforms_resolver.rb +++ b/app/graphql/resolvers/ci/runner_platforms_resolver.rb @@ -3,7 +3,8 @@ module Resolvers module Ci class RunnerPlatformsResolver < BaseResolver - type Types::Ci::RunnerPlatformType, null: false + type Types::Ci::RunnerPlatformType.connection_type, null: true + description 'Supported runner platforms.' def resolve(**args) runner_instructions.map do |platform, data| diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb index ac2a56b89a7..9166999b400 100644 --- a/app/graphql/resolvers/ci/runner_setup_resolver.rb +++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb @@ -3,30 +3,37 @@ module Resolvers module Ci class RunnerSetupResolver < BaseResolver + ACCESS_DENIED = 'User is not authorized to register a runner for the specified resource!' + type Types::Ci::RunnerSetupType, null: true + description 'Runner setup instructions.' - argument :platform, GraphQL::STRING_TYPE, - required: true, - description: 'Platform to generate the instructions for.' + argument :platform, + type: GraphQL::STRING_TYPE, + required: true, + description: 'Platform to generate the instructions for.' - argument :architecture, GraphQL::STRING_TYPE, - required: true, - description: 'Architecture to generate the instructions for.' + argument :architecture, + type: GraphQL::STRING_TYPE, + required: true, + description: 'Architecture to generate the instructions for.' - argument :project_id, ::Types::GlobalIDType[::Project], - required: false, - description: 'Project to register the runner for.' + argument :project_id, + type: ::Types::GlobalIDType[::Project], + required: false, + deprecated: { reason: 'No longer used', milestone: '13.11' }, + description: 'Project to register the runner for.' - argument :group_id, ::Types::GlobalIDType[::Group], - required: false, - description: 'Group to register the runner for.' + argument :group_id, + type: ::Types::GlobalIDType[::Group], + required: false, + deprecated: { reason: 'No longer used', milestone: '13.11' }, + description: 'Group to register the runner for.' def resolve(platform:, architecture:, **args) instructions = Gitlab::Ci::RunnerInstructions.new( - current_user: current_user, os: platform, - arch: architecture, - **target_param(args) + arch: architecture ) { @@ -34,11 +41,15 @@ module Resolvers register_instructions: instructions.register_command } ensure - raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError') + raise Gitlab::Graphql::Errors::ResourceNotAvailable, ACCESS_DENIED if access_denied?(instructions) end private + def access_denied?(instructions) + instructions.errors.include?('Gitlab::Access::AccessDeniedError') + end + def other_install_instructions(platform) Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS[platform.to_sym][:installation_instructions_url] end diff --git a/app/graphql/resolvers/ci/test_report_summary_resolver.rb b/app/graphql/resolvers/ci/test_report_summary_resolver.rb new file mode 100644 index 00000000000..22db70f032a --- /dev/null +++ b/app/graphql/resolvers/ci/test_report_summary_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class TestReportSummaryResolver < BaseResolver + type ::Types::Ci::TestReportSummaryType, null: true + + alias_method :pipeline, :object + + def resolve(**args) + TestReportSummarySerializer + .new(project: pipeline.project, current_user: @current_user) + .represent(pipeline.test_report_summary) + end + end + end +end diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb new file mode 100644 index 00000000000..90cc30b1281 --- /dev/null +++ b/app/graphql/resolvers/ci/test_suite_resolver.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class TestSuiteResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type ::Types::Ci::TestSuiteType, null: true + authorize :read_build + authorizes_object! + + alias_method :pipeline, :object + + argument :build_ids, [GraphQL::ID_TYPE], + required: true, + description: 'IDs of the builds used to run the test suite.' + + def resolve(build_ids:) + builds = pipeline.latest_builds.id_in(build_ids).presence + return unless builds + + TestSuiteSerializer + .new(project: pipeline.project, current_user: @current_user) + .represent(load_test_suite_data(builds), details: true) + end + + private + + def load_test_suite_data(builds) + suite = builds.sum do |build| + build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + end + + Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load! + + suite + end + end + end +end diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb index 1541738f46c..3484a1cc4ba 100644 --- a/app/graphql/resolvers/concerns/board_issue_filterable.rb +++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb @@ -7,10 +7,10 @@ module BoardIssueFilterable def issue_filters(args) filters = args.to_h + set_filter_values(filters) if filters[:not] - filters[:not] = filters[:not].to_h set_filter_values(filters[:not]) end @@ -18,6 +18,17 @@ module BoardIssueFilterable end def set_filter_values(filters) + filter_by_assignee(filters) + end + + def filter_by_assignee(filters) + if filters[:assignee_username] && filters[:assignee_wildcard_id] + raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: assigneeUsername, assigneeWildcardId.' + end + + if filters[:assignee_wildcard_id] + filters[:assignee_id] = filters.delete(:assignee_wildcard_id) + end end end diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 84b0dafe213..0ff3997f3bc 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -12,10 +12,10 @@ module IssueResolverArguments argument :iids, [GraphQL::STRING_TYPE], required: false, description: 'List of IIDs of issues. For example, [1, 2].' - argument :label_name, GraphQL::STRING_TYPE.to_list_type, + argument :label_name, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Labels applied to this issue.' - argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, + argument :milestone_title, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Milestone applied to this issue.' argument :author_username, GraphQL::STRING_TYPE, @@ -23,7 +23,8 @@ module IssueResolverArguments description: 'Username of the author of the issue.' argument :assignee_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of a user assigned to the issue.' + description: 'Username of a user assigned to the issue.', + deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' } argument :assignee_usernames, [GraphQL::STRING_TYPE], required: false, description: 'Usernames of users assigned to the issue.' @@ -55,6 +56,10 @@ module IssueResolverArguments as: :issue_types, description: 'Filter issues by the given issue types.', required: false + argument :not, Types::Issues::NegatedIssueFilterInputType, + description: 'List of negated params.', + prepare: ->(negated_args, ctx) { negated_args.to_h }, + required: false end def resolve_with_lookahead(**args) @@ -69,11 +74,22 @@ module IssueResolverArguments args[:iids] ||= [args.delete(:iid)].compact if args[:iid] args[:attempt_project_search_optimizations] = true if args[:search].present? + prepare_assignee_username_params(args) + finder = IssuesFinder.new(current_user, args) continue_issue_resolve(parent, finder, **args) end + def ready?(**args) + if args.slice(*mutually_exclusive_assignee_username_args).compact.size > 1 + arg_str = mutually_exclusive_assignee_username_args.map { |x| x.to_s.camelize(:lower) }.join(', ') + raise Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time." + end + + super + end + class_methods do def resolver_complexity(args, child_complexity:) complexity = super @@ -82,4 +98,15 @@ module IssueResolverArguments complexity end end + + private + + def prepare_assignee_username_params(args) + args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present? + args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present? + end + + def mutually_exclusive_assignee_username_args + [:assignee_usernames, :assignee_username] + end end diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index 77a85edfba6..644b2a11460 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -15,12 +15,7 @@ module LooksAhead end def apply_lookahead(query) - selection = node_selection - - includes = preloads.each.flat_map do |name, requirements| - selection&.selects?(name) ? requirements : [] - end - all_preloads = (unconditional_includes + includes).uniq + all_preloads = (unconditional_includes + filtered_preloads).uniq return query if all_preloads.empty? @@ -37,6 +32,14 @@ module LooksAhead {} end + def filtered_preloads + selection = node_selection + + preloads.each.flat_map do |name, requirements| + selection&.selects?(name) ? requirements : [] + end + end + def node_selection return unless lookahead diff --git a/app/graphql/resolvers/concerns/manual_authorization.rb b/app/graphql/resolvers/concerns/manual_authorization.rb deleted file mode 100644 index 182110b9594..00000000000 --- a/app/graphql/resolvers/concerns/manual_authorization.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this entirely when framework authorization is released -# See: https://gitlab.com/gitlab-org/gitlab/-/issues/290216 -module ManualAuthorization - def resolve(**args) - super - rescue ::Gitlab::Graphql::Errors::ResourceNotAvailable - nil - end -end diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 31444b0c592..75f1ee478a8 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -50,7 +50,8 @@ module ResolvesMergeRequests approved_by: [:approved_by_users], milestone: [:milestone], security_auto_fix: [:author], - head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] + head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }], + timelogs: [:timelogs] } end end diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb index 445f3567b1d..8de85c074ec 100644 --- a/app/graphql/resolvers/concerns/resolves_snippets.rb +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -4,7 +4,7 @@ module ResolvesSnippets extend ActiveSupport::Concern included do - type Types::SnippetType.connection_type, null: false + type Types::SnippetType.connection_type, null: true argument :ids, [::Types::GlobalIDType[::Snippet]], required: false, diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb index 0c7dad622cf..a09b0a1fd87 100644 --- a/app/graphql/resolvers/echo_resolver.rb +++ b/app/graphql/resolvers/echo_resolver.rb @@ -5,8 +5,10 @@ module Resolvers type ::GraphQL::STRING_TYPE, null: false description 'Testing endpoint to validate the API with' - argument :text, GraphQL::STRING_TYPE, required: true, - description: 'Text to echo back.' + argument :text, + type: GraphQL::STRING_TYPE, + required: true, + description: 'Text to echo back.' def resolve(text:) username = current_user&.username diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index ed3395d05aa..df04e70e250 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -21,7 +21,7 @@ module Resolvers def resolve(**args) return unless project.present? - EnvironmentsFinder.new(project, context[:current_user], args).find + EnvironmentsFinder.new(project, context[:current_user], args).execute rescue EnvironmentsFinder::InvalidStatesError => exception raise Gitlab::Graphql::Errors::ArgumentError, exception.message end diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb index 36e1977b756..d3662b08cdf 100644 --- a/app/graphql/resolvers/group_members_resolver.rb +++ b/app/graphql/resolvers/group_members_resolver.rb @@ -13,12 +13,6 @@ module Resolvers private - def preloads - { - user: [:user, :source] - } - end - def finder_class GroupMembersFinder end diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb index 2bad974daf7..34a4c67bc56 100644 --- a/app/graphql/resolvers/group_merge_requests_resolver.rb +++ b/app/graphql/resolvers/group_merge_requests_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class GroupMergeRequestsResolver < MergeRequestsResolver include GroupIssuableResolver - alias_method :group, :synchronized_object + alias_method :group, :object type Types::MergeRequestType.connection_type, null: true diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb index 179283fd7b7..31280b36278 100644 --- a/app/graphql/resolvers/group_milestones_resolver.rb +++ b/app/graphql/resolvers/group_milestones_resolver.rb @@ -1,22 +1,40 @@ # frozen_string_literal: true -# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver) module Resolvers class GroupMilestonesResolver < MilestonesResolver argument :include_descendants, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Also return milestones in all subgroups and subprojects.' + description: 'Include milestones from all subgroups and subprojects.' + argument :include_ancestors, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Include milestones from all parent groups.' type Types::MilestoneType.connection_type, null: true private def parent_id_parameters(args) - return { group_ids: parent.id } unless args[:include_descendants].present? + include_ancestors = args[:include_ancestors].present? + include_descendants = args[:include_descendants].present? + return { group_ids: parent.id } unless include_ancestors || include_descendants + + group_ids = if include_ancestors && include_descendants + parent.self_and_hierarchy + elsif include_ancestors + parent.self_and_ancestors + else + parent.self_and_descendants + end + + project_ids = if include_descendants + group_projects.with_issues_or_mrs_available_for_user(current_user) + else + nil + end { - group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id), - project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user) + group_ids: group_ids.public_or_visible_to_user(current_user).select(:id), + project_ids: project_ids } end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ac3bdda0f12..7a67f115abf 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -44,7 +44,8 @@ module Resolvers { alert_management_alert: [:alert_management_alert], labels: [:labels], - assignees: [:assignees] + assignees: [:assignees], + timelogs: [:timelogs] } end diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index 76c3ae936ee..2b731d54cdd 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -21,6 +21,12 @@ module Resolvers private + def preloads + { + user: [:user, :source] + } + end + def finder_class # override in subclass end diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index 8fd33c6626e..c431d079beb 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -4,14 +4,14 @@ module Resolvers class MergeRequestResolver < BaseResolver.single include ResolvesMergeRequests - alias_method :project, :synchronized_object + alias_method :project, :object type ::Types::MergeRequestType, null: true argument :iid, GraphQL::STRING_TYPE, - required: true, - as: :iids, - description: 'IID of the merge request, for example `1`.' + required: true, + as: :iids, + description: 'IID of the merge request, for example `1`.' def no_results_possible?(args) project.nil? diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index ecbdaaa3f55..a9eea4ae4b8 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -3,42 +3,49 @@ module Resolvers class MergeRequestsResolver < BaseResolver include ResolvesMergeRequests + extend ::Gitlab::Graphql::NegatableArguments type ::Types::MergeRequestType.connection_type, null: true - alias_method :project, :synchronized_object + alias_method :project, :object def self.accept_assignee argument :assignee_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of the assignee.' + required: false, + description: 'Username of the assignee.' end def self.accept_author argument :author_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of the author.' + required: false, + description: 'Username of the author.' end def self.accept_reviewer argument :reviewer_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of the reviewer.' + required: false, + description: 'Username of the reviewer.' end argument :iids, [GraphQL::STRING_TYPE], - required: false, - description: 'Array of IIDs of merge requests, for example `[1, 2]`.' + required: false, + description: 'Array of IIDs of merge requests, for example `[1, 2]`.' argument :source_branches, [GraphQL::STRING_TYPE], required: false, as: :source_branch, - description: 'Array of source branch names. All resolved merge requests will have one of these branches as their source.' + description: <<~DESC + Array of source branch names. + All resolved merge requests will have one of these branches as their source. + DESC argument :target_branches, [GraphQL::STRING_TYPE], required: false, as: :target_branch, - description: 'Array of target branch names. All resolved merge requests will have one of these branches as their target.' + description: <<~DESC + Array of target branch names. + All resolved merge requests will have one of these branches as their target. + DESC argument :state, ::Types::MergeRequestStateEnum, required: false, @@ -62,6 +69,16 @@ module Resolvers required: false, default_value: :created_desc + negated do + argument :labels, [GraphQL::STRING_TYPE], + required: false, + as: :label_name, + description: 'Array of label names. All resolved merge requests will not have these labels.' + argument :milestone_title, GraphQL::STRING_TYPE, + required: false, + description: 'Title of the milestone.' + end + def self.single ::Resolvers::MergeRequestResolver end diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb index a82a4a95254..0669fececd5 100644 --- a/app/graphql/resolvers/metrics/dashboard_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb @@ -8,15 +8,16 @@ module Resolvers argument :path, GraphQL::STRING_TYPE, required: true, - description: "Path to a file which defines metrics dashboard " \ - "eg: 'config/prometheus/common_metrics.yml'." + description: <<~MD + Path to a file which defines a metrics dashboard eg: `"config/prometheus/common_metrics.yml"`. + MD alias_method :environment, :object - def resolve(**args) + def resolve(path:) return unless environment - ::PerformanceMonitoring::PrometheusDashboard.find_for(**args, **service_params) + ::PerformanceMonitoring::PrometheusDashboard.find_for(path: path, **service_params) end private diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 9a715e4d08b..c94e3d9e1d8 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -7,7 +7,7 @@ module Resolvers argument :ids, [GraphQL::ID_TYPE], required: false, - description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1".' + description: 'Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`.' argument :state, Types::MilestoneStateEnum, required: false, @@ -56,7 +56,7 @@ module Resolvers end def parent - synchronized_object + object end def parent_id_parameters(args) diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb index e688e34599a..89d79747732 100644 --- a/app/graphql/resolvers/package_details_resolver.rb +++ b/app/graphql/resolvers/package_details_resolver.rb @@ -2,12 +2,20 @@ module Resolvers class PackageDetailsResolver < BaseResolver - type ::Types::Packages::PackageType, null: true + type ::Types::Packages::PackageDetailsType, null: true argument :id, ::Types::GlobalIDType[::Packages::Package], required: true, description: 'The global ID of the package.' + def ready?(**args) + context[self.class] ||= { executions: 0 } + context[self.class][:executions] += 1 + raise GraphQL::ExecutionError, "Package details can be requested only for one package at a time" if context[self.class][:executions] > 1 + + super + end + def resolve(id:) # TODO: remove this line when the compatibility layer is removed # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb new file mode 100644 index 00000000000..75068014242 --- /dev/null +++ b/app/graphql/resolvers/project_jobs_resolver.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Resolvers + class ProjectJobsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead + + type ::Types::Ci::JobType.connection_type, null: true + authorize :read_build + authorizes_object! + + argument :statuses, [::Types::Ci::JobStatusEnum], + required: false, + description: 'Filter jobs by status.' + + alias_method :project, :object + + def ready?(**args) + context[self.class] ||= { executions: 0 } + context[self.class][:executions] += 1 + raise GraphQL::ExecutionError, "Jobs can only be requested for one project at a time" if context[self.class][:executions] > 1 + + super + end + + def resolve_with_lookahead(statuses: nil) + jobs = ::Ci::JobsFinder.new(current_user: current_user, project: project, params: { scope: statuses }).execute + + apply_lookahead(jobs) + end + + private + + def preloads + { + artifacts: [:job_artifacts], + pipeline: [:user] + } + end + end +end diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 8fca6b829c0..aa8808b15ac 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -31,7 +31,7 @@ module Resolvers end else BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args| - finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas) + finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas) finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) } end diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index f618bf2df77..ec31a7dbe6d 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -3,11 +3,11 @@ module Resolvers module Projects class ServicesResolver < BaseResolver - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource type Types::Projects::ServiceType.connection_type, null: true authorize :admin_project + authorizes_object! argument :active, GraphQL::BOOLEAN_TYPE, @@ -20,15 +20,7 @@ module Resolvers alias_method :project, :object - def resolve(**args) - authorize!(project) - - services(args[:active], args[:type]) - end - - private - - def services(active, type) + def resolve(active: nil, type: nil) servs = project.services servs = servs.by_active_flag(active) unless active.nil? servs = servs.by_type(type) unless type.blank? diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb new file mode 100644 index 00000000000..45cfe229b2f --- /dev/null +++ b/app/graphql/resolvers/repository_branch_names_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + class RepositoryBranchNamesResolver < BaseResolver + type ::GraphQL::STRING_TYPE, null: false + + calls_gitaly! + + argument :search_pattern, GraphQL::STRING_TYPE, + required: true, + description: 'The pattern to search for branch names by.' + + def resolve(search_pattern:) + Repositories::BranchNamesFinder.new(object, search: search_pattern).execute + end + end +end diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb index 569b82149d3..4328d38d485 100644 --- a/app/graphql/resolvers/snippets/blobs_resolver.rb +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -3,12 +3,12 @@ module Resolvers module Snippets class BlobsResolver < BaseResolver - prepend ManualAuthorization include Gitlab::Graphql::Authorize::AuthorizeResource type Types::Snippets::BlobType.connection_type, null: true authorize :read_snippet calls_gitaly! + authorizes_object! alias_method :snippet, :object @@ -17,7 +17,6 @@ module Resolvers description: 'Paths of the blobs.' def resolve(paths: []) - authorize!(snippet) return [snippet.blob] if snippet.empty_repo? if paths.empty? diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb new file mode 100644 index 00000000000..aebd04259ee --- /dev/null +++ b/app/graphql/resolvers/timelog_resolver.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Resolvers + class TimelogResolver < BaseResolver + include LooksAhead + + type ::Types::TimelogType.connection_type, null: false + + argument :start_date, Types::TimeType, + required: false, + description: 'List time logs within a date range where the logged date is equal to or after startDate.' + + argument :end_date, Types::TimeType, + required: false, + description: 'List time logs within a date range where the logged date is equal to or before endDate.' + + argument :start_time, Types::TimeType, + required: false, + description: 'List time-logs within a time range where the logged time is equal to or after startTime.' + + argument :end_time, Types::TimeType, + required: false, + description: 'List time-logs within a time range where the logged time is equal to or before endTime.' + + def resolve_with_lookahead(**args) + return Timelog.none unless timelogs_available_for_user? + + validate_params_presence!(args) + transformed_args = transform_args(args) + validate_time_difference!(transformed_args) + + find_timelogs(transformed_args) + end + + private + + def preloads + { + note: [:note] + } + end + + def find_timelogs(args) + apply_lookahead(group.timelogs(args[:start_time], args[:end_time])) + end + + def timelogs_available_for_user? + group&.user_can_access_group_timelogs?(context[:current_user]) + end + + def validate_params_presence!(args) + message = case time_params_count(args) + when 0 + 'Start and End arguments must be present' + when 1 + 'Both Start and End arguments must be present' + when 2 + validate_duplicated_args(args) + when 3 || 4 + 'Only Time or Date arguments must be present' + end + + raise_argument_error(message) if message + end + + def validate_time_difference!(args) + message = if args[:end_time] < args[:start_time] + 'Start argument must be before End argument' + elsif args[:end_time] - args[:start_time] > 60.days + 'The time range period cannot contain more than 60 days' + end + + raise_argument_error(message) if message + end + + def transform_args(args) + return args if args.keys == [:start_time, :end_time] + + time_args = args.except(:start_date, :end_date) + + if time_args.empty? + time_args[:start_time] = args[:start_date].beginning_of_day + time_args[:end_time] = args[:end_date].end_of_day + elsif time_args.key?(:start_time) + time_args[:end_time] = args[:end_date].end_of_day + elsif time_args.key?(:end_time) + time_args[:start_time] = args[:start_date].beginning_of_day + end + + time_args + end + + def time_params_count(args) + [:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) } + end + + def validate_duplicated_args(args) + if args.key?(:start_time) && args.key?(:start_date) || + args.key?(:end_time) && args.key?(:end_date) + 'Both Start and End arguments must be present' + end + end + + def raise_argument_error(message) + raise Gitlab::Graphql::Errors::ArgumentError, message + end + + def group + @group ||= object.respond_to?(:sync) ? object.sync : object + end + end +end diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb index 47967fe69f9..221a43f691d 100644 --- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb +++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb @@ -4,16 +4,24 @@ module Resolvers class UserMergeRequestsResolverBase < MergeRequestsResolver include ResolvesProject - argument :project_path, GraphQL::STRING_TYPE, - required: false, - description: 'The full-path of the project the authored merge requests should be in. Incompatible with projectId.' + argument :project_path, + type: GraphQL::STRING_TYPE, + required: false, + description: <<~DESC + The full-path of the project the authored merge requests should be in. + Incompatible with projectId. + DESC - argument :project_id, ::Types::GlobalIDType[::Project], - required: false, - description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.' + argument :project_id, + type: ::Types::GlobalIDType[::Project], + required: false, + description: <<~DESC + The global ID of the project the authored merge requests should be in. + Incompatible with projectPath. + DESC attr_reader :project - alias_method :user, :synchronized_object + alias_method :user, :object def ready?(project_id: nil, project_path: nil, **args) return early_return unless can_read_profile? @@ -22,8 +30,7 @@ module Resolvers load_project(project_path, project_id) return early_return unless can_read_project? elsif args[:iids].present? - raise ::Gitlab::Graphql::Errors::ArgumentError, - 'iids requires projectPath or projectId' + raise ::Gitlab::Graphql::Errors::ArgumentError, 'iids requires projectPath or projectId' end super(**args) diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb index db420b3d116..a8abe759f27 100644 --- a/app/graphql/resolvers/user_starred_projects_resolver.rb +++ b/app/graphql/resolvers/user_starred_projects_resolver.rb @@ -2,11 +2,11 @@ module Resolvers class UserStarredProjectsResolver < BaseResolver - type Types::ProjectType, null: true + type Types::ProjectType.connection_type, null: true argument :search, GraphQL::STRING_TYPE, - required: false, - description: 'Search query.' + required: false, + description: 'Search query.' alias_method :user, :object diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb index e8048b9deb9..ee1727aadbe 100644 --- a/app/graphql/resolvers/users/snippets_resolver.rb +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -5,6 +5,7 @@ module Resolvers module Users class SnippetsResolver < BaseResolver include ResolvesSnippets + include Gitlab::Allowable alias_method :user, :object @@ -14,6 +15,12 @@ module Resolvers private + def resolve_snippets(_args) + return Snippet.none unless Ability.allowed?(current_user, :read_user_profile, user) + + super + end + def snippet_finder_params(args) super.merge(author: user) end diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb index 4ad9e8c0e40..ff9a5a0611d 100644 --- a/app/graphql/types/base_argument.rb +++ b/app/graphql/types/base_argument.rb @@ -4,8 +4,10 @@ module Types class BaseArgument < GraphQL::Schema::Argument include GitlabStyleDeprecations + attr_reader :deprecation + def initialize(*args, **kwargs, &block) - kwargs = gitlab_deprecation(kwargs) + @deprecation = gitlab_deprecation(kwargs) super(*args, **kwargs, &block) end diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index 4d470aceca4..518a902a5d7 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -21,12 +21,23 @@ module Types graphql_name(enum_mod.name) if use_name description(enum_mod.description) if use_description - enum_mod.definition.each { |key, content| value(key.to_s.upcase, **content) } + enum_mod.definition.each do |key, content| + value(key.to_s.upcase, **content) + end + end + + # Helper to define an enum member for each element of a Rails AR enum + def from_rails_enum(enum, description:) + enum.each_key do |name| + value name.to_s.upcase, + value: name, + description: format(description, name: name) + end end def value(*args, **kwargs, &block) enum[args[0].downcase] = kwargs[:value] || args[0] - kwargs = gitlab_deprecation(kwargs) + gitlab_deprecation(kwargs) super(*args, **kwargs, &block) end @@ -36,6 +47,18 @@ module Types def enum @enum_values ||= {}.with_indifferent_access end + + def authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end + + def authorize(*abilities) + @abilities = abilities + end + + def authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end end end end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 78ab6890923..7c939f94dde 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -2,28 +2,30 @@ module Types class BaseField < GraphQL::Schema::Field - prepend Gitlab::Graphql::Authorize include GitlabStyleDeprecations argument_class ::Types::BaseArgument DEFAULT_COMPLEXITY = 1 + attr_reader :deprecation + def initialize(**kwargs, &block) @calls_gitaly = !!kwargs.delete(:calls_gitaly) @constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0 @requires_argument = !!kwargs.delete(:requires_argument) + @authorize = Array.wrap(kwargs.delete(:authorize)) kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity]) @feature_flag = kwargs[:feature_flag] kwargs = check_feature_flag(kwargs) - kwargs = gitlab_deprecation(kwargs) + @deprecation = gitlab_deprecation(kwargs) super(**kwargs, &block) # We want to avoid the overhead of this in prod extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env? - extension ::Gitlab::Graphql::Present::FieldExtension + extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension end def may_call_gitaly? @@ -34,6 +36,19 @@ module Types @requires_argument || arguments.values.any? { |argument| argument.type.non_null? } end + # By default fields authorize against the current object, but that is not how our + # resolvers work - they use declarative permissions to authorize fields + # manually (so we make them opt in). + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/300922 + # (separate out authorize into permissions on the object, and on the + # resolved values) + # We do not support argument authorization in our schema. If/when we do, + # we should call `super` here, to apply argument authorization checks. + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/324647 + def authorized?(object, args, ctx) + field_authorized?(object, ctx) && resolver_authorized?(object, ctx) + end + def base_complexity complexity = DEFAULT_COMPLEXITY complexity += 1 if calls_gitaly? @@ -58,6 +73,26 @@ module Types attr_reader :feature_flag + def field_authorized?(object, ctx) + authorization.ok?(object, ctx[:current_user]) + end + + # Historically our resolvers have used declarative permission checks only + # for _what they resolved_, not the _object they resolved these things from_ + # We preserve these semantics here, and only apply resolver authorization + # if the resolver has opted in. + def resolver_authorized?(object, ctx) + if @resolver_class && @resolver_class.try(:authorizes_object?) + @resolver_class.authorized?(object, ctx) + else + true + end + end + + def authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(@authorize) + end + def feature_documentation_message(key, description) "#{description} Available only when feature flag `#{key}` is enabled." end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb index 4b1f3193136..c21c95876be 100644 --- a/app/graphql/types/base_interface.rb +++ b/app/graphql/types/base_interface.rb @@ -5,5 +5,11 @@ module Types include GraphQL::Schema::Interface field_class ::Types::BaseField + + definition_methods do + def authorized?(object, context) + resolve_type(object, context).authorized?(object, context) + end + end end end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 9c36c83d4a3..cd677e50d28 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -19,6 +19,14 @@ module Types GitlabSchema.id_from_object(object) end + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize) + end + + def self.authorized?(object, context) + authorization.ok?(object, context[:current_user]) + end + def current_user context[:current_user] end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb index 30a5668c0bb..aeafbf85020 100644 --- a/app/graphql/types/base_union.rb +++ b/app/graphql/types/base_union.rb @@ -2,5 +2,8 @@ module Types class BaseUnion < GraphQL::Schema::Union + def self.authorized?(object, context) + resolve_type(object, context).authorized?(object, context) + end end end diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb index f33f3f5e537..42d8eecc366 100644 --- a/app/graphql/types/board_type.rb +++ b/app/graphql/types/board_type.rb @@ -20,6 +20,12 @@ module Types field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true, description: 'Whether or not closed list is hidden.' + field :created_at, Types::TimeType, null: false, + description: 'Timestamp of when the board was created.' + + field :updated_at, Types::TimeType, null: false, + description: 'Timestamp of when the board was last updated.' + field :lists, Types::BoardListType.connection_type, null: true, diff --git a/app/graphql/types/boards/assignee_wildcard_id_enum.rb b/app/graphql/types/boards/assignee_wildcard_id_enum.rb new file mode 100644 index 00000000000..ba9058a78d9 --- /dev/null +++ b/app/graphql/types/boards/assignee_wildcard_id_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Boards + class AssigneeWildcardIdEnum < BaseEnum + graphql_name 'AssigneeWildcardId' + description 'Assignee ID wildcard values' + + value 'NONE', 'No assignee is assigned.' + value 'ANY', 'An assignee is assigned.' + end + end +end diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb new file mode 100644 index 00000000000..2cd057347d6 --- /dev/null +++ b/app/graphql/types/boards/board_issuable_input_base_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Boards + # Common arguments that we can be used to filter boards epics and issues + class BoardIssuableInputBaseType < BaseInputObject + argument :label_name, [GraphQL::STRING_TYPE, null: true], + required: false, + description: 'Filter by label name.' + + argument :author_username, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by author username.' + + argument :my_reaction_emoji, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by reaction emoji applied by the current user.' + end + end +end diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb index b762cef6e58..7cf2dcb9c82 100644 --- a/app/graphql/types/boards/board_issue_input_base_type.rb +++ b/app/graphql/types/boards/board_issue_input_base_type.rb @@ -2,30 +2,19 @@ module Types module Boards - class BoardIssueInputBaseType < BaseInputObject - argument :label_name, GraphQL::STRING_TYPE.to_list_type, - required: false, - description: 'Filter by label name.' - + # rubocop: disable Graphql/AuthorizeTypes + class BoardIssueInputBaseType < BoardIssuableInputBaseType argument :milestone_title, GraphQL::STRING_TYPE, required: false, description: 'Filter by milestone title.' - argument :assignee_username, GraphQL::STRING_TYPE.to_list_type, + argument :assignee_username, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Filter by assignee username.' - argument :author_username, GraphQL::STRING_TYPE, - required: false, - description: 'Filter by author username.' - argument :release_tag, GraphQL::STRING_TYPE, required: false, description: 'Filter by release tag.' - - argument :my_reaction_emoji, GraphQL::STRING_TYPE, - required: false, - description: 'Filter by reaction emoji.' end end end diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb index 9cc0f484a16..8c0e37e5cb7 100644 --- a/app/graphql/types/boards/board_issue_input_type.rb +++ b/app/graphql/types/boards/board_issue_input_type.rb @@ -2,19 +2,24 @@ module Types module Boards - class NegatedBoardIssueInputType < BoardIssueInputBaseType - end - class BoardIssueInputType < BoardIssueInputBaseType graphql_name 'BoardIssueInput' argument :not, NegatedBoardIssueInputType, required: false, - description: 'List of negated params. Warning: this argument is experimental and a subject to change in future.' + prepare: ->(negated_args, ctx) { negated_args.to_h }, + description: <<~MD + List of negated arguments. + Warning: this argument is experimental and a subject to change in future. + MD argument :search, GraphQL::STRING_TYPE, required: false, description: 'Search query for issue title or description.' + + argument :assignee_wildcard_id, ::Types::Boards::AssigneeWildcardIdEnum, + required: false, + description: 'Filter by assignee wildcard. Incompatible with assigneeUsername.' end end end diff --git a/app/graphql/types/boards/negated_board_issue_input_type.rb b/app/graphql/types/boards/negated_board_issue_input_type.rb new file mode 100644 index 00000000000..a0fab2ae969 --- /dev/null +++ b/app/graphql/types/boards/negated_board_issue_input_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module Boards + class NegatedBoardIssueInputType < BoardIssueInputBaseType + end + end +end + +Types::Boards::NegatedBoardIssueInputType.prepend_if_ee('::EE::Types::Boards::NegatedBoardIssueInputType') diff --git a/app/graphql/types/ci/job_status_enum.rb b/app/graphql/types/ci/job_status_enum.rb new file mode 100644 index 00000000000..ec80b1f4776 --- /dev/null +++ b/app/graphql/types/ci/job_status_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + class JobStatusEnum < BaseEnum + graphql_name 'CiJobStatus' + + ::Ci::HasStatus::AVAILABLE_STATUSES.each do |status| + value status.upcase, + description: "A job that is #{status.tr('_', ' ')}.", + value: status + end + end + end +end diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index c86337eea89..94a256fed3d 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -6,27 +6,74 @@ module Types graphql_name 'CiJob' authorize :read_commit_status + connection_type_class(Types::CountableConnectionType) + + field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true, + description: 'ID of the job.' field :pipeline, Types::Ci::PipelineType, null: true, description: 'Pipeline the job belongs to.' field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the job.' field :needs, BuildNeedType.connection_type, null: true, description: 'References to builds that must complete before the jobs run.' - field :detailed_status, Types::Ci::DetailedStatusType, null: true, - description: 'Detailed status of the job.' + field :status, + type: ::Types::Ci::JobStatusEnum, + null: true, + description: "Status of the job." + field :stage, Types::Ci::StageType, null: true, + description: 'Stage of the job.' + field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false, + description: 'Whether this job is allowed to fail.' + field :duration, GraphQL::INT_TYPE, null: true, + description: 'Duration of the job in seconds.' + field :tags, [GraphQL::STRING_TYPE], null: true, + description: 'Tags for the current job.' + + # Life-cycle timestamps: + field :created_at, Types::TimeType, null: false, + description: "When the job was created." + field :queued_at, Types::TimeType, null: true, + description: 'When the job was enqueued and marked as pending.' + field :started_at, Types::TimeType, null: true, + description: 'When the job was started.' + field :finished_at, Types::TimeType, null: true, + description: 'When a job has finished running.' field :scheduled_at, Types::TimeType, null: true, description: 'Schedule for the build.' + + field :detailed_status, Types::Ci::DetailedStatusType, null: true, + description: 'Detailed status of the job.' field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true, description: 'Artifacts generated by the job.' - field :finished_at, Types::TimeType, null: true, - description: 'When a job has finished running.' - field :duration, GraphQL::INT_TYPE, null: true, - description: 'Duration of the job in seconds.' + field :short_sha, type: GraphQL::STRING_TYPE, null: false, + description: 'Short SHA1 ID of the commit.' + field :scheduling_type, GraphQL::STRING_TYPE, null: true, + description: 'Type of pipeline scheduling. Value is `dag` if the pipeline uses the `needs` keyword, and `stage` otherwise.' + field :commit_path, GraphQL::STRING_TYPE, null: true, + description: 'Path to the commit that triggered the job.' + field :ref_name, GraphQL::STRING_TYPE, null: true, + description: 'Ref name of the job.' + field :ref_path, GraphQL::STRING_TYPE, null: true, + description: 'Path to the ref.' + field :playable, GraphQL::BOOLEAN_TYPE, null: false, method: :playable?, + description: 'Indicates the job can be played.' + field :retryable, GraphQL::BOOLEAN_TYPE, null: false, method: :retryable?, + description: 'Indicates the job can be retried.' + field :cancelable, GraphQL::BOOLEAN_TYPE, null: false, method: :cancelable?, + description: 'Indicates the job can be canceled.' + field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?, + description: 'Indicates the job is active.' + field :coverage, GraphQL::FLOAT_TYPE, null: true, + description: 'Coverage level of the job.' def pipeline Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find end + def tags + object.tags.map(&:name) if object.is_a?(::Ci::Build) + end + def detailed_status object.detailed_status(context[:current_user]) end @@ -36,6 +83,46 @@ module Types object.job_artifacts end end + + def stage + ::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl| + BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader| + by_pipeline = ids + .group_by(&:first) + .transform_values { |grp| grp.map(&:second) } + + by_pipeline.each do |p, names| + p.stages.by_name(names).each { |s| loader.call([p, s.name], s) } + end + end + end + end + + # This class is a secret union! + # TODO: turn this into an actual union, so that fields can be referenced safely! + def id + return unless object.id.present? + + model_name = object.type || ::CommitStatus.name + id = object.id + Gitlab::GlobalId.build(model_name: model_name, id: id) + end + + def commit_path + ::Gitlab::Routing.url_helpers.project_commit_path(object.project, object.sha) + end + + def ref_name + object&.ref + end + + def ref_path + ::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_name) + end + + def coverage + object&.coverage + end end end end diff --git a/app/graphql/types/ci/pipeline_config_source_enum.rb b/app/graphql/types/ci/pipeline_config_source_enum.rb index e1575cb2f99..96c8a5f2941 100644 --- a/app/graphql/types/ci/pipeline_config_source_enum.rb +++ b/app/graphql/types/ci/pipeline_config_source_enum.rb @@ -4,7 +4,8 @@ module Types module Ci class PipelineConfigSourceEnum < BaseEnum ::Enums::Ci::Pipeline.config_sources.keys.each do |state_symbol| - value state_symbol.to_s.upcase, value: state_symbol.to_s + description = state_symbol == :auto_devops_source ? "Auto DevOps source." : "#{state_symbol.to_s.titleize.capitalize}." # This is needed to avoid failure in doc lint + value state_symbol.to_s.upcase, value: state_symbol.to_s, description: description end end end diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb index c19ddf5bb25..e0b2020dcc1 100644 --- a/app/graphql/types/ci/pipeline_status_enum.rb +++ b/app/graphql/types/ci/pipeline_status_enum.rb @@ -4,7 +4,9 @@ module Types module Ci class PipelineStatusEnum < BaseEnum ::Ci::Pipeline.all_state_names.each do |state_symbol| - value state_symbol.to_s.upcase, value: state_symbol.to_s + value state_symbol.to_s.upcase, + description: ::Ci::Pipeline::STATUSES_DESCRIPTION[state_symbol], + value: state_symbol.to_s end end end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 49be200a788..2e83f6c1f5a 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -81,6 +81,20 @@ module Types description: 'Jobs belonging to the pipeline.', resolver: ::Resolvers::Ci::JobsResolver + field :job, + type: ::Types::Ci::JobType, + null: true, + description: 'A specific job in this pipeline, either by name or ID.' do + argument :id, + type: ::Types::GlobalIDType[::CommitStatus], + required: false, + description: 'ID of the job.' + argument :name, + type: ::GraphQL::STRING_TYPE, + required: false, + description: 'Name of the job.' + end + field :source_job, Types::Ci::JobType, null: true, description: 'Job where pipeline was triggered from.' @@ -104,8 +118,24 @@ module Types field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?, description: 'Indicates if the pipeline is active.' + field :uses_needs, GraphQL::BOOLEAN_TYPE, null: true, + method: :uses_needs?, + description: 'Indicates if the pipeline has jobs with `needs` dependencies.' + + field :test_report_summary, + Types::Ci::TestReportSummaryType, + null: false, + description: 'Summary of the test report generated by the pipeline.', + resolver: Resolvers::Ci::TestReportSummaryResolver + + field :test_suite, + Types::Ci::TestSuiteType, + null: true, + description: 'A specific test suite in a pipeline test report.', + resolver: Resolvers::Ci::TestSuiteResolver + def detailed_status - object.detailed_status(context[:current_user]) + object.detailed_status(current_user) end def user @@ -119,6 +149,19 @@ module Types def path ::Gitlab::Routing.url_helpers.project_pipeline_path(object.project, object) end + + def job(id: nil, name: nil) + raise ::Gitlab::Graphql::Errors::ArgumentError, 'One of id or name is required' unless id || name + + if id + id = ::Types::GlobalIDType[::CommitStatus].coerce_isolated_input(id) if id + pipeline.statuses.id_in(id.model_id) + else + pipeline.statuses.by_name(name) + end.take # rubocop: disable CodeReuse/ActiveRecord + end + + alias_method :pipeline, :object end end end diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb new file mode 100644 index 00000000000..eeff7222762 --- /dev/null +++ b/app/graphql/types/ci/recent_failures_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RecentFailuresType < BaseObject + graphql_name 'RecentFailures' + description 'Recent failure history of a test case.' + + connection_type_class(Types::CountableConnectionType) + + field :count, GraphQL::INT_TYPE, null: true, + description: 'Number of times the test case has failed in the past 14 days.' + + field :base_branch, GraphQL::STRING_TYPE, null: true, + description: 'Name of the base branch of the project.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index 836f2430890..56b4f248697 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -12,10 +12,13 @@ module Types extras: [:lookahead], description: 'Group of jobs for the stage.' field :detailed_status, Types::Ci::DetailedStatusType, null: true, - description: 'Detailed status of the stage.' + description: 'Detailed status of the stage.' + field :jobs, Ci::JobType.connection_type, null: true, + description: 'Jobs for the stage.', + method: 'latest_statuses' def detailed_status - object.detailed_status(context[:current_user]) + object.detailed_status(current_user) end # Issues one query per pipeline @@ -33,6 +36,34 @@ module Types jobs_for_pipeline(pl, indexed.keys, include_needs).each do |stage_id, statuses| key = indexed[stage_id] groups = ::Ci::Group.fabricate(project, key.stage, statuses) + + if Feature.enabled?(:ci_no_empty_groups, project) + groups.each do |group| + rejected = group.jobs.reject { |job| Ability.allowed?(current_user, :read_commit_status, job) } + group.jobs.select! { |job| Ability.allowed?(current_user, :read_commit_status, job) } + next unless group.jobs.empty? + + exc = StandardError.new('Empty Ci::Group') + traces = rejected.map do |job| + trace = [] + policy = Ability.policy_for(current_user, job) + policy.debug(:read_commit_status, trace) + trace + end + extra = { + current_user_id: current_user&.id, + project_id: project.id, + pipeline_id: pl.id, + stage_id: stage_id, + group_name: group.name, + rejected_job_ids: rejected.map(&:id), + rejected_traces: traces + } + Gitlab::ErrorTracking.track_exception(exc, extra) + end + groups.reject! { |group| group.jobs.empty? } + end + loader.call(key, groups) end end diff --git a/app/graphql/types/ci/test_case_status_enum.rb b/app/graphql/types/ci/test_case_status_enum.rb new file mode 100644 index 00000000000..6a5f8bc2a59 --- /dev/null +++ b/app/graphql/types/ci/test_case_status_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + class TestCaseStatusEnum < BaseEnum + graphql_name 'TestCaseStatus' + + ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status| + value status, + description: "Test case that has a status of #{status}.", + value: status + end + end + end +end diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb new file mode 100644 index 00000000000..9cc3f918125 --- /dev/null +++ b/app/graphql/types/ci/test_case_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestCaseType < BaseObject + graphql_name 'TestCase' + description 'Test case in pipeline test report.' + + connection_type_class(Types::CountableConnectionType) + + field :status, Types::Ci::TestCaseStatusEnum, null: true, + description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})." + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the test case.' + + field :classname, GraphQL::STRING_TYPE, null: true, + description: 'Classname of the test case.' + + field :execution_time, GraphQL::FLOAT_TYPE, null: true, + description: 'Test case execution time in seconds.' + + field :file, GraphQL::STRING_TYPE, null: true, + description: 'Path to the file of the test case.' + + field :attachment_url, GraphQL::STRING_TYPE, null: true, + description: 'URL of the test case attachment file.' + + field :system_output, GraphQL::STRING_TYPE, null: true, + description: 'System output of the test case.' + + field :stack_trace, GraphQL::STRING_TYPE, null: true, + description: 'Stack trace of the test case.' + + field :recent_failures, Types::Ci::RecentFailuresType, null: true, + description: 'Recent failure history of the test case on the base branch.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_report_summary_type.rb b/app/graphql/types/ci/test_report_summary_type.rb new file mode 100644 index 00000000000..87207c8a765 --- /dev/null +++ b/app/graphql/types/ci/test_report_summary_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + # This is presented through `PipelineType` that has its own authorization + class TestReportSummaryType < BaseObject + graphql_name 'TestReportSummary' + description 'Test report for a pipeline' + + field :total, Types::Ci::TestReportTotalType, null: false, + description: 'Total report statistics for a pipeline test report.' + + field :test_suites, Types::Ci::TestSuiteSummaryType.connection_type, null: false, + description: 'Test suites belonging to a pipeline test report.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_report_total_type.rb b/app/graphql/types/ci/test_report_total_type.rb new file mode 100644 index 00000000000..1123734adc3 --- /dev/null +++ b/app/graphql/types/ci/test_report_total_type.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestReportTotalType < BaseObject + graphql_name 'TestReportTotal' + description 'Total test report statistics.' + + field :time, GraphQL::FLOAT_TYPE, null: true, + description: 'Total duration of the tests.' + + field :count, GraphQL::INT_TYPE, null: true, + description: 'Total number of the test cases.' + + field :success, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that succeeded.' + + field :failed, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that failed.' + + field :skipped, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that were skipped.' + + field :error, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that had an error.' + + field :suite_error, GraphQL::STRING_TYPE, null: true, + description: 'Test suite error message.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_suite_summary_type.rb b/app/graphql/types/ci/test_suite_summary_type.rb new file mode 100644 index 00000000000..a80a9179cb4 --- /dev/null +++ b/app/graphql/types/ci/test_suite_summary_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestSuiteSummaryType < BaseObject + graphql_name 'TestSuiteSummary' + description 'Test suite summary in a pipeline test report.' + + connection_type_class(Types::CountableConnectionType) + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the test suite.' + + field :total_time, GraphQL::FLOAT_TYPE, null: true, + description: 'Total duration of the tests in the test suite.' + + field :total_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of the test cases in the test suite.' + + field :success_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that succeeded in the test suite.' + + field :failed_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that failed in the test suite.' + + field :skipped_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that were skipped in the test suite.' + + field :error_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that had an error.' + + field :suite_error, GraphQL::STRING_TYPE, null: true, + description: 'Test suite error message.' + + field :build_ids, [GraphQL::ID_TYPE], null: true, + description: 'IDs of the builds used to run the test suite.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb new file mode 100644 index 00000000000..7d4c01da81b --- /dev/null +++ b/app/graphql/types/ci/test_suite_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestSuiteType < BaseObject + graphql_name 'TestSuite' + description 'Test suite in a pipeline test report.' + + connection_type_class(Types::CountableConnectionType) + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the test suite.' + + field :total_time, GraphQL::FLOAT_TYPE, null: true, + description: 'Total duration of the tests in the test suite.' + + field :total_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of the test cases in the test suite.' + + field :success_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that succeeded in the test suite.' + + field :failed_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that failed in the test suite.' + + field :skipped_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that were skipped in the test suite.' + + field :error_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that had an error.' + + field :suite_error, GraphQL::STRING_TYPE, null: true, + description: 'Test suite error message.' + + field :test_cases, Types::Ci::TestCaseType.connection_type, null: true, + description: 'Test cases in the test suite.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/concerns/find_closest.rb b/app/graphql/types/concerns/find_closest.rb new file mode 100644 index 00000000000..1d76e872364 --- /dev/null +++ b/app/graphql/types/concerns/find_closest.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module FindClosest + # Find the closest node of a given type above this node, and return the domain object + def closest_parent(type, parent) + parent = parent.try(:parent) while parent && parent.object.class != type + return unless parent + + parent.object.object + end +end diff --git a/app/graphql/types/concerns/gitlab_style_deprecations.rb b/app/graphql/types/concerns/gitlab_style_deprecations.rb index ad195354930..802562ed958 100644 --- a/app/graphql/types/concerns/gitlab_style_deprecations.rb +++ b/app/graphql/types/concerns/gitlab_style_deprecations.rb @@ -7,25 +7,21 @@ module GitlabStyleDeprecations private + # Mutate the arguments, returns the deprecation def gitlab_deprecation(kwargs) if kwargs[:deprecation_reason].present? raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values' end - deprecation = kwargs.delete(:deprecated) - return kwargs unless deprecation + deprecation = ::Gitlab::Graphql::Deprecation.parse(kwargs.delete(:deprecated)) + return unless deprecation - milestone, reason = deprecation.values_at(:milestone, :reason).map(&:presence) + raise ArgumentError, "Bad deprecation. #{deprecation.errors.full_messages.to_sentence}" unless deprecation.valid? - raise ArgumentError, 'Please provide a `milestone` within `deprecated`' unless milestone - raise ArgumentError, 'Please provide a `reason` within `deprecated`' unless reason - raise ArgumentError, '`milestone` must be a `String`' unless milestone.is_a?(String) + kwargs[:deprecation_reason] = deprecation.deprecation_reason + kwargs[:description] = deprecation.edit_description(kwargs[:description]) - deprecated_in = "Deprecated in #{milestone}" - kwargs[:deprecation_reason] = "#{reason}. #{deprecated_in}." - kwargs[:description] += " #{deprecated_in}: #{reason}." if kwargs[:description] - - kwargs + deprecation end end diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index 750bd1bfe8d..79061df7282 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -23,7 +23,7 @@ module Types A global identifier. A global identifier represents an object uniquely across the application. - An example of such an identifier is "gid://gitlab/User/1". + An example of such an identifier is `"gid://gitlab/User/1"`. Global identifiers are encoded as strings. DESC @@ -67,6 +67,17 @@ module Types graphql_name end + define_singleton_method(:as) do |new_name| + if @renamed && graphql_name != new_name + raise "Conflicting names for ID of #{model_class.name}: " \ + "#{graphql_name} and #{new_name}" + end + + @renamed = true + graphql_name(new_name) + self + end + define_singleton_method(:coerce_result) do |gid, ctx| global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name) diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 7a84e76657b..a44281b2bdf 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -8,39 +8,65 @@ module Types expose_permissions Types::PermissionTypes::Group - field :web_url, GraphQL::STRING_TYPE, null: false, + field :web_url, + type: GraphQL::STRING_TYPE, + null: false, description: 'Web URL of the group.' - field :avatar_url, GraphQL::STRING_TYPE, null: true, + field :avatar_url, + type: GraphQL::STRING_TYPE, + null: true, description: 'Avatar URL of the group.' - field :custom_emoji, Types::CustomEmojiType.connection_type, null: true, + field :custom_emoji, + type: Types::CustomEmojiType.connection_type, + null: true, description: 'Custom emoji within this namespace.', feature_flag: :custom_emoji - field :share_with_group_lock, GraphQL::BOOLEAN_TYPE, null: true, + field :share_with_group_lock, + type: GraphQL::BOOLEAN_TYPE, + null: true, description: 'Indicates if sharing a project with another group within this group is prevented.' - field :project_creation_level, GraphQL::STRING_TYPE, null: true, method: :project_creation_level_str, + field :project_creation_level, + type: GraphQL::STRING_TYPE, + null: true, + method: :project_creation_level_str, description: 'The permission level required to create projects in the group.' - field :subgroup_creation_level, GraphQL::STRING_TYPE, null: true, method: :subgroup_creation_level_str, + field :subgroup_creation_level, + type: GraphQL::STRING_TYPE, + null: true, + method: :subgroup_creation_level_str, description: 'The permission level required to create subgroups within the group.' - field :require_two_factor_authentication, GraphQL::BOOLEAN_TYPE, null: true, + field :require_two_factor_authentication, + type: GraphQL::BOOLEAN_TYPE, + null: true, description: 'Indicates if all users in this group are required to set up two-factor authentication.' - field :two_factor_grace_period, GraphQL::INT_TYPE, null: true, + field :two_factor_grace_period, + type: GraphQL::INT_TYPE, + null: true, description: 'Time before two-factor authentication is enforced.' - field :auto_devops_enabled, GraphQL::BOOLEAN_TYPE, null: true, + field :auto_devops_enabled, + type: GraphQL::BOOLEAN_TYPE, + null: true, description: 'Indicates whether Auto DevOps is enabled for all projects within this group.' - field :emails_disabled, GraphQL::BOOLEAN_TYPE, null: true, + field :emails_disabled, + type: GraphQL::BOOLEAN_TYPE, + null: true, description: 'Indicates if a group has email notifications disabled.' - field :mentions_disabled, GraphQL::BOOLEAN_TYPE, null: true, + field :mentions_disabled, + type: GraphQL::BOOLEAN_TYPE, + null: true, description: 'Indicates if a group is disabled from getting mentioned.' - field :parent, GroupType, null: true, + field :parent, + type: GroupType, + null: true, description: 'Parent group.' field :issues, @@ -55,7 +81,7 @@ module Types description: 'Merge requests for projects in this group.', resolver: Resolvers::GroupMergeRequestsResolver - field :milestones, Types::MilestoneType.connection_type, null: true, + field :milestones, description: 'Milestones of the group.', resolver: Resolvers::GroupMilestonesResolver @@ -76,9 +102,10 @@ module Types Types::LabelType, null: true, description: 'A label available on this group.' do - argument :title, GraphQL::STRING_TYPE, - required: true, - description: 'Title of the label.' + argument :title, + type: GraphQL::STRING_TYPE, + required: true, + description: 'Title of the label.' end field :group_members, @@ -92,7 +119,9 @@ module Types resolver: Resolvers::ContainerRepositoriesResolver, authorize: :read_container_image - field :container_repositories_count, GraphQL::INT_TYPE, null: false, + field :container_repositories_count, + type: GraphQL::INT_TYPE, + null: false, description: 'Number of container repositories in the group.' field :packages, @@ -114,6 +143,12 @@ module Types description: 'Labels available on this group.', resolver: Resolvers::GroupLabelsResolver + field :timelogs, ::Types::TimelogType.connection_type, null: false, + description: 'Time logged on issues in the group and its subgroups.', + extras: [:lookahead], + complexity: 5, + resolver: ::Resolvers::TimelogResolver + def avatar_url object.avatar_url(only_path: false) end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index f15ab69f2d4..34c824fe9fb 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -124,6 +124,9 @@ module Types field :create_note_email, GraphQL::STRING_TYPE, null: true, description: 'User specific email address for the issue.' + field :timelogs, Types::TimelogType.connection_type, null: false, + description: 'Timelogs on the issue.' + def author Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find end diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb new file mode 100644 index 00000000000..10bf6f21792 --- /dev/null +++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Issues + class NegatedIssueFilterInputType < BaseInputObject + graphql_name 'NegatedIssueFilterInput' + + argument :iids, [GraphQL::STRING_TYPE], + required: false, + description: 'List of IIDs of issues to exclude. For example, [1, 2].' + argument :label_name, [GraphQL::STRING_TYPE], + required: false, + description: 'Labels not applied to this issue.' + argument :milestone_title, [GraphQL::STRING_TYPE], + required: false, + description: 'Milestone not applied to this issue.' + argument :assignee_usernames, [GraphQL::STRING_TYPE], + required: false, + description: 'Usernames of users not assigned to the issue.' + argument :assignee_id, GraphQL::STRING_TYPE, + required: false, + description: 'ID of a user not assigned to the issues.' + end + end +end + +Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType') diff --git a/app/graphql/types/jira_users_mapping_input_type.rb b/app/graphql/types/jira_users_mapping_input_type.rb index 61e3240ecf3..32640b9cb17 100644 --- a/app/graphql/types/jira_users_mapping_input_type.rb +++ b/app/graphql/types/jira_users_mapping_input_type.rb @@ -5,12 +5,12 @@ module Types graphql_name 'JiraUsersMappingInputType' argument :jira_account_id, - GraphQL::STRING_TYPE, - required: true, - description: 'Jira account ID of the user.' + GraphQL::STRING_TYPE, + required: true, + description: 'Jira account ID of the user.' argument :gitlab_id, - GraphQL::INT_TYPE, - required: false, - description: 'Id of the GitLab user.' + GraphQL::INT_TYPE, + required: false, + description: 'ID of the GitLab user.' end end diff --git a/app/graphql/types/merge_request_review_state_enum.rb b/app/graphql/types/merge_request_review_state_enum.rb new file mode 100644 index 00000000000..45f97758425 --- /dev/null +++ b/app/graphql/types/merge_request_review_state_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class MergeRequestReviewStateEnum < BaseEnum + graphql_name 'MergeRequestReviewState' + description 'State of a review of a GitLab merge request.' + + from_rails_enum(::MergeRequestReviewer.states, + description: "The merge request is %{name}.") + end +end diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb index a2d7bd0306c..bcf18b836de 100644 --- a/app/graphql/types/merge_request_state_enum.rb +++ b/app/graphql/types/merge_request_state_enum.rb @@ -5,6 +5,6 @@ module Types graphql_name 'MergeRequestState' description 'State of a GitLab merge request' - value 'merged', description: "Merge Request has been merged." + value 'merged', description: "Merge request has been merged." end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 449286915f2..c8ccf9d8aff 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -132,7 +132,10 @@ module Types description: 'The milestone of the merge request.' field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'Assignees of the merge request.' - field :reviewers, Types::UserType.connection_type, null: true, complexity: 5, + field :reviewers, + type: Types::MergeRequests::ReviewerType.connection_type, + null: true, + complexity: 5, description: 'Users from whom a review has been requested.' field :author, Types::UserType, null: true, description: 'User who created this merge request.' @@ -183,6 +186,8 @@ module Types description: 'Selected auto merge strategy.' field :merge_user, Types::UserType, null: true, description: 'User who merged this merge request.' + field :timelogs, Types::TimelogType.connection_type, null: false, + description: 'Timelogs on the merge request.' def approved_by object.approved_by_users diff --git a/app/graphql/types/merge_requests/reviewer_type.rb b/app/graphql/types/merge_requests/reviewer_type.rb new file mode 100644 index 00000000000..09ced39844a --- /dev/null +++ b/app/graphql/types/merge_requests/reviewer_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + module MergeRequests + class ReviewerType < ::Types::UserType + include FindClosest + + graphql_name 'MergeRequestReviewer' + description 'A user from whom a merge request review has been requested.' + authorize :read_user + + field :merge_request_interaction, + type: ::Types::UserMergeRequestInteractionType, + null: true, + extras: [:parent], + description: "Details of this user's interactions with the merge request." + + def merge_request_interaction(parent:) + merge_request = closest_parent(::Types::MergeRequestType, parent) + return unless merge_request + + Users::MergeRequestInteraction.new(user: object, merge_request: merge_request) + end + end + end +end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index c3816116e2b..91a5109c748 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -14,6 +14,9 @@ module Types field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the milestone.' + field :iid, GraphQL::ID_TYPE, null: false, + description: "Internal ID of the milestone." + field :title, GraphQL::STRING_TYPE, null: false, description: 'Title of the milestone.' diff --git a/app/graphql/types/mutation_operation_mode_enum.rb b/app/graphql/types/mutation_operation_mode_enum.rb index 75c1d7cd4a6..08214eebc7e 100644 --- a/app/graphql/types/mutation_operation_mode_enum.rb +++ b/app/graphql/types/mutation_operation_mode_enum.rb @@ -10,5 +10,13 @@ module Types value 'REPLACE', 'Performs a replace operation.' value 'APPEND', 'Performs an append operation.' value 'REMOVE', 'Performs a removal operation.' + + def self.default_mode + enum[:replace] + end + + def self.transform_modes + enum.values_at(:remove, :append) + end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 76ffddf416f..5a9c7b32deb 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -68,6 +68,7 @@ module Types mount_mutation Mutations::Releases::Delete mount_mutation Mutations::ReleaseAssetLinks::Create mount_mutation Mutations::ReleaseAssetLinks::Update + mount_mutation Mutations::ReleaseAssetLinks::Delete mount_mutation Mutations::Terraform::State::Delete mount_mutation Mutations::Terraform::State::Lock mount_mutation Mutations::Terraform::State::Unlock diff --git a/app/graphql/types/packages/conan/file_metadatum_type.rb b/app/graphql/types/packages/conan/file_metadatum_type.rb new file mode 100644 index 00000000000..97d5abe6ba4 --- /dev/null +++ b/app/graphql/types/packages/conan/file_metadatum_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module Packages + module Conan + class FileMetadatumType < BaseObject + graphql_name 'ConanFileMetadata' + description 'Conan file metadata' + + implements Types::Packages::FileMetadataType + + authorize :read_package + + field :id, ::Types::GlobalIDType[::Packages::Conan::FileMetadatum], null: false, description: 'ID of the metadatum.' + field :recipe_revision, GraphQL::STRING_TYPE, null: false, description: 'Revision of the Conan recipe.' + field :package_revision, GraphQL::STRING_TYPE, null: true, description: 'Revision of the package.' + field :conan_package_reference, GraphQL::STRING_TYPE, null: true, description: 'Reference of the Conan package.' + field :conan_file_type, ::Types::Packages::Conan::MetadatumFileTypeEnum, null: false, description: 'Type of the Conan file.' + end + end + end +end diff --git a/app/graphql/types/packages/conan/metadatum_file_type_enum.rb b/app/graphql/types/packages/conan/metadatum_file_type_enum.rb new file mode 100644 index 00000000000..d8ec3a44d4d --- /dev/null +++ b/app/graphql/types/packages/conan/metadatum_file_type_enum.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Packages + module Conan + class MetadatumFileTypeEnum < BaseEnum + graphql_name 'ConanMetadatumFileTypeEnum' + description 'Conan file types' + + ::Packages::Conan::FileMetadatum.conan_file_types.keys.each do |file| + value file.upcase, value: file, description: "A #{file.humanize(capitalize: false)} type." + end + end + end + end +end diff --git a/app/graphql/types/packages/conan/metadatum_type.rb b/app/graphql/types/packages/conan/metadatum_type.rb new file mode 100644 index 00000000000..00b84235d27 --- /dev/null +++ b/app/graphql/types/packages/conan/metadatum_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module Packages + module Conan + class MetadatumType < BaseObject + graphql_name 'ConanMetadata' + description 'Conan metadata' + + authorize :read_package + + field :id, ::Types::GlobalIDType[::Packages::Conan::Metadatum], null: false, description: 'ID of the metadatum.' + field :created_at, Types::TimeType, null: false, description: 'Date of creation.' + field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' + field :package_username, GraphQL::STRING_TYPE, null: false, description: 'Username of the Conan package.' + field :package_channel, GraphQL::STRING_TYPE, null: false, description: 'Channel of the Conan package.' + field :recipe, GraphQL::STRING_TYPE, null: false, description: 'Recipe of the Conan package.' + field :recipe_path, GraphQL::STRING_TYPE, null: false, description: 'Recipe path of the Conan package.' + end + end + end +end diff --git a/app/graphql/types/packages/file_metadata_type.rb b/app/graphql/types/packages/file_metadata_type.rb new file mode 100644 index 00000000000..46ccb424218 --- /dev/null +++ b/app/graphql/types/packages/file_metadata_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Packages + module FileMetadataType + include ::Types::BaseInterface + graphql_name 'PackageFileMetadata' + description 'Represents metadata associated with a Package file' + + field :created_at, ::Types::TimeType, null: false, description: 'Date of creation.' + field :updated_at, ::Types::TimeType, null: false, description: 'Date of most recent update.' + + def self.resolve_type(object, context) + case object + when ::Packages::Conan::FileMetadatum + ::Types::Packages::Conan::FileMetadatumType + else + # NOTE: This method must be kept in sync with `PackageFileType#file_metadata`, + # which must never produce data that this discriminator cannot handle. + raise 'Unsupported file metadata type' + end + end + + orphan_types Types::Packages::Conan::FileMetadatumType + end + end +end diff --git a/app/graphql/types/packages/metadata_type.rb b/app/graphql/types/packages/metadata_type.rb index 26c43b51a69..4ab6707df88 100644 --- a/app/graphql/types/packages/metadata_type.rb +++ b/app/graphql/types/packages/metadata_type.rb @@ -6,12 +6,14 @@ module Types graphql_name 'PackageMetadata' description 'Represents metadata associated with a Package' - possible_types ::Types::Packages::Composer::MetadatumType + possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType def self.resolve_type(object, context) case object when ::Packages::Composer::Metadatum ::Types::Packages::Composer::MetadatumType + when ::Packages::Conan::Metadatum + ::Types::Packages::Conan::MetadatumType else # NOTE: This method must be kept in sync with `PackageWithoutVersionsType#metadata`, # which must never produce data that this discriminator cannot handle. diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb new file mode 100644 index 00000000000..510b7e2ba41 --- /dev/null +++ b/app/graphql/types/packages/package_details_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageDetailsType < PackageType + graphql_name 'PackageDetailsType' + description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes' + authorize :read_package + + field :versions, ::Types::Packages::PackageType.connection_type, null: true, + description: 'The other versions of the package.' + + field :package_files, Types::Packages::PackageFileType.connection_type, null: true, description: 'Package files.' + + def versions + object.versions + end + end + end +end diff --git a/app/graphql/types/packages/package_file_type.rb b/app/graphql/types/packages/package_file_type.rb new file mode 100644 index 00000000000..e9e38559626 --- /dev/null +++ b/app/graphql/types/packages/package_file_type.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageFileType < BaseObject + graphql_name 'PackageFile' + description 'Represents a package file' + authorize :read_package + + field :id, ::Types::GlobalIDType[::Packages::PackageFile], null: false, description: 'ID of the file.' + field :created_at, Types::TimeType, null: false, description: 'The created date.' + field :updated_at, Types::TimeType, null: false, description: 'The updated date.' + field :size, GraphQL::STRING_TYPE, null: false, description: 'Size of the package file.' + field :file_name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package file.' + field :download_path, GraphQL::STRING_TYPE, null: false, description: 'Download path of the package file.' + field :file_md5, GraphQL::STRING_TYPE, null: true, description: 'Md5 of the package file.' + field :file_sha1, GraphQL::STRING_TYPE, null: true, description: 'Sha1 of the package file.' + field :file_sha256, GraphQL::STRING_TYPE, null: true, description: 'Sha256 of the package file.' + field :file_metadata, Types::Packages::FileMetadataType, null: true, + description: 'File metadata.' + + # NOTE: This method must be kept in sync with the union + # type: `Types::Packages::FileMetadataType`. + # + # `Types::Packages::FileMetadataType.resolve_type(metadata, ctx)` must never raise. + def file_metadata + case object.package.package_type + when 'conan' + object.conan_file_metadatum + else + nil + end + end + end + end +end diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb index 331898a1e84..a263ca1577a 100644 --- a/app/graphql/types/packages/package_type.rb +++ b/app/graphql/types/packages/package_type.rb @@ -2,13 +2,52 @@ module Types module Packages - class PackageType < PackageWithoutVersionsType + class PackageType < ::Types::BaseObject graphql_name 'Package' - description 'Represents a package in the Package Registry' + description 'Represents a package in the Package Registry. Note that this type is in beta and susceptible to changes' + authorize :read_package - field :versions, ::Types::Packages::PackageWithoutVersionsType.connection_type, null: true, - description: 'The other versions of the package.' + field :id, ::Types::GlobalIDType[::Packages::Package], null: false, + description: 'ID of the package.' + + field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package.' + field :created_at, Types::TimeType, null: false, description: 'Date of creation.' + field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' + field :version, GraphQL::STRING_TYPE, null: true, description: 'Version string.' + field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.' + field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.' + field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.' + field :pipelines, Types::Ci::PipelineType.connection_type, null: true, + description: 'Pipelines that built the package.' + field :metadata, Types::Packages::MetadataType, null: true, + description: 'Package metadata.' + field :versions, ::Types::Packages::PackageType.connection_type, null: true, + description: 'The other versions of the package.', + deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' } + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find + end + + def versions + [] + end + + # NOTE: This method must be kept in sync with the union + # type: `Types::Packages::MetadataType`. + # + # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise. + def metadata + case object.package_type + when 'composer' + object.composer_metadatum + when 'conan' + object.conan_metadatum + else + nil + end + end end end end diff --git a/app/graphql/types/packages/package_without_versions_type.rb b/app/graphql/types/packages/package_without_versions_type.rb deleted file mode 100644 index 9c6bb37e6cc..00000000000 --- a/app/graphql/types/packages/package_without_versions_type.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module Types - module Packages - class PackageWithoutVersionsType < ::Types::BaseObject - graphql_name 'PackageWithoutVersions' - description 'Represents a version of a package in the Package Registry' - - authorize :read_package - - field :id, ::Types::GlobalIDType[::Packages::Package], null: false, - description: 'ID of the package.' - - field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package.' - field :created_at, Types::TimeType, null: false, description: 'Date of creation.' - field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' - field :version, GraphQL::STRING_TYPE, null: true, description: 'Version string.' - field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.' - field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.' - field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.' - field :pipelines, Types::Ci::PipelineType.connection_type, null: true, - description: 'Pipelines that built the package.' - field :metadata, Types::Packages::MetadataType, null: true, - description: 'Package metadata.' - - def project - Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find - end - - # NOTE: This method must be kept in sync with the union - # type: `Types::Packages::MetadataType`. - # - # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise. - def metadata - case object.package_type - when 'composer' - object.composer_metadatum - else - nil - end - end - end - end -end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 9a3f2e311e6..21534f40499 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -183,6 +183,12 @@ module Types description: 'Packages of the project.', resolver: Resolvers::ProjectPackagesResolver + field :jobs, + Types::Ci::JobType.connection_type, + null: true, + description: 'Jobs of a project. This field can only be resolved for one project in any single request.', + resolver: Resolvers::ProjectJobsResolver + field :pipelines, null: true, description: 'Build pipelines of the project.', diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 74818bfcd42..8af0db644dd 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -55,7 +55,10 @@ module Types field :container_repository, Types::ContainerRepositoryDetailsType, null: true, description: 'Find a container repository.' do - argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository.' + argument :id, + type: ::Types::GlobalIDType[::ContainerRepository], + required: true, + description: 'The global ID of the container repository.' end field :package, @@ -72,9 +75,7 @@ module Types description: 'Find users.', resolver: Resolvers::UsersResolver - field :echo, GraphQL::STRING_TYPE, null: false, - description: 'Text to echo back.', - resolver: Resolvers::EchoResolver + field :echo, resolver: Resolvers::EchoResolver field :issue, Types::IssueType, null: true, @@ -82,11 +83,16 @@ module Types argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.' end - field :instance_statistics_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, + field :instance_statistics_measurements, + type: Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, null: true, description: 'Get statistics on the instance.', - deprecated: { reason: 'This field was renamed. Use the `usageTrendsMeasurements` field instead', milestone: '13.10' }, - resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver + resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver, + deprecated: { + reason: :renamed, + replacement: 'Query.usageTrendsMeasurements', + milestone: '13.10' + } field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, null: true, @@ -97,18 +103,10 @@ module Types null: true, description: 'CI related settings that apply to the entire instance.' - field :runner_platforms, Types::Ci::RunnerPlatformType.connection_type, - null: true, description: 'Supported runner platforms.', - resolver: Resolvers::Ci::RunnerPlatformsResolver - - field :runner_setup, Types::Ci::RunnerSetupType, null: true, - description: 'Get runner setup instructions.', - resolver: Resolvers::Ci::RunnerSetupResolver + field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver + field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver - field :ci_config, Types::Ci::Config::ConfigType, null: true, - description: 'Get linted and processed contents of a CI config. Should not be requested more than once per request.', - resolver: Resolvers::Ci::ConfigResolver, - complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1 + field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1 def design_management DesignManagementObject.new(nil) diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb new file mode 100644 index 00000000000..912fc5f643a --- /dev/null +++ b/app/graphql/types/repository/blob_type.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module Types + module Repository + # rubocop: disable Graphql/AuthorizeTypes + # This is presented through `Repository` that has its own authorization + class BlobType < BaseObject + present_using BlobPresenter + + graphql_name 'RepositoryBlob' + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the blob.' + + field :oid, GraphQL::STRING_TYPE, null: false, method: :id, + description: 'OID of the blob.' + + field :path, GraphQL::STRING_TYPE, null: false, + description: 'Path of the blob.' + + field :name, GraphQL::STRING_TYPE, + description: 'Blob name.', + null: true + + field :mode, type: GraphQL::STRING_TYPE, + description: 'Blob mode.', + null: true + + field :lfs_oid, GraphQL::STRING_TYPE, null: true, + calls_gitaly: true, + description: 'LFS OID of the blob.' + + field :web_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path of the blob.' + + def lfs_oid + Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find + end + end + end +end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index e319a5f3124..963a4296c4f 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -14,5 +14,10 @@ module Types description: 'Indicates a corresponding Git repository exists on disk.' field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, description: 'Tree of the repository.' + field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, + description: 'Blobs contained within the repository' + field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true, + complexity: 170, description: 'Names of branches available in this repository that match the search pattern.', + resolver: Resolvers::RepositoryBranchNamesResolver end end diff --git a/app/graphql/types/sort_enum.rb b/app/graphql/types/sort_enum.rb index ff994039b6d..cc04394004d 100644 --- a/app/graphql/types/sort_enum.rb +++ b/app/graphql/types/sort_enum.rb @@ -7,10 +7,34 @@ module Types # Deprecated, as we prefer uppercase enums # https://gitlab.com/groups/gitlab-org/-/epics/1838 - value 'updated_desc', 'Updated at descending order.', value: :updated_desc, deprecated: { reason: 'Use UPDATED_DESC', milestone: '13.5' } - value 'updated_asc', 'Updated at ascending order.', value: :updated_asc, deprecated: { reason: 'Use UPDATED_ASC', milestone: '13.5' } - value 'created_desc', 'Created at descending order.', value: :created_desc, deprecated: { reason: 'Use CREATED_DESC', milestone: '13.5' } - value 'created_asc', 'Created at ascending order.', value: :created_asc, deprecated: { reason: 'Use CREATED_ASC', milestone: '13.5' } + value 'updated_desc', 'Updated at descending order.', + value: :updated_desc, + deprecated: { + reason: :renamed, + replacement: 'UPDATED_DESC', + milestone: '13.5' + } + value 'updated_asc', 'Updated at ascending order.', + value: :updated_asc, + deprecated: { + reason: :renamed, + replacement: 'UPDATED_ASC', + milestone: '13.5' + } + value 'created_desc', 'Created at descending order.', + value: :created_desc, + deprecated: { + reason: :renamed, + replacement: 'CREATED_DESC', + milestone: '13.5' + } + value 'created_asc', 'Created at ascending order.', + value: :created_asc, + deprecated: { + reason: :renamed, + replacement: 'CREATED_ASC', + milestone: '13.5' + } value 'UPDATED_DESC', 'Updated at descending order.', value: :updated_desc value 'UPDATED_ASC', 'Updated at ascending order.', value: :updated_asc diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb new file mode 100644 index 00000000000..465e3c492bc --- /dev/null +++ b/app/graphql/types/timelog_type.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Types + class TimelogType < BaseObject + graphql_name 'Timelog' + + authorize :read_group_timelogs + + field :spent_at, + Types::TimeType, + null: true, + description: 'Timestamp of when the time tracked was spent at.' + + field :time_spent, + GraphQL::INT_TYPE, + null: false, + description: 'The time spent displayed in seconds.' + + field :user, + Types::UserType, + null: false, + description: 'The user that logged the time.' + + field :issue, + Types::IssueType, + null: true, + description: 'The issue that logged time was added to.' + + field :note, + Types::Notes::NoteType, + null: true, + description: 'The note where the quick action to add the logged time was executed.' + + def user + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find + end + + def issue + Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.issue_id).find + end + end +end diff --git a/app/graphql/types/user_merge_request_interaction_type.rb b/app/graphql/types/user_merge_request_interaction_type.rb new file mode 100644 index 00000000000..5ff0d79f13e --- /dev/null +++ b/app/graphql/types/user_merge_request_interaction_type.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Types + class UserMergeRequestInteractionType < BaseObject + graphql_name 'UserMergeRequestInteraction' + description <<~MD + Information about a merge request given a specific user. + + This object has two parts to its state: a `User` and a `MergeRequest`. All + fields relate to interactions between the two entities. + MD + + authorize :read_merge_request + + field :can_merge, + type: ::GraphQL::BOOLEAN_TYPE, + null: false, + calls_gitaly: true, + method: :can_merge?, + description: 'Whether this user can merge this merge request.' + + field :can_update, + type: ::GraphQL::BOOLEAN_TYPE, + null: false, + method: :can_update?, + description: 'Whether this user can update this merge request.' + + field :review_state, + ::Types::MergeRequestReviewStateEnum, + null: true, + description: 'The state of the review by this user.' + + field :reviewed, + type: ::GraphQL::BOOLEAN_TYPE, + null: false, + method: :reviewed?, + description: 'Whether this user has provided a review for this merge request.' + + field :approved, + type: ::GraphQL::BOOLEAN_TYPE, + null: false, + method: :approved?, + description: 'Whether this user has approved this merge request.' + end +end + +::Types::UserMergeRequestInteractionType.prepend_if_ee('EE::Types::UserMergeRequestInteractionType') diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index 2cc7d379240..3d7db80ae11 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -3,6 +3,7 @@ module Types class UserType < BaseObject graphql_name 'User' + description 'Representation of a GitLab user.' authorize :read_user @@ -10,61 +11,87 @@ module Types expose_permissions Types::PermissionTypes::User - field :id, GraphQL::ID_TYPE, null: false, + field :id, + type: GraphQL::ID_TYPE, + null: false, description: 'ID of the user.' - field :bot, GraphQL::BOOLEAN_TYPE, null: false, + field :bot, + type: GraphQL::BOOLEAN_TYPE, + null: false, description: 'Indicates if the user is a bot.', method: :bot? - field :username, GraphQL::STRING_TYPE, null: false, + field :username, + type: GraphQL::STRING_TYPE, + null: false, description: 'Username of the user. Unique within this instance of GitLab.' - field :name, GraphQL::STRING_TYPE, null: false, + field :name, + type: GraphQL::STRING_TYPE, + null: false, description: 'Human-readable name of the user.' - field :state, Types::UserStateEnum, null: false, + field :state, + type: Types::UserStateEnum, + null: false, description: 'State of the user.' - field :email, GraphQL::STRING_TYPE, null: true, + field :email, + type: GraphQL::STRING_TYPE, + null: true, description: 'User email.', method: :public_email, - deprecated: { reason: 'Use public_email', milestone: '13.7' } - field :public_email, GraphQL::STRING_TYPE, null: true, + deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' } + field :public_email, + type: GraphQL::STRING_TYPE, + null: true, description: "User's public email." - field :avatar_url, GraphQL::STRING_TYPE, null: true, + field :avatar_url, + type: GraphQL::STRING_TYPE, + null: true, description: "URL of the user's avatar." - field :web_url, GraphQL::STRING_TYPE, null: false, + field :web_url, + type: GraphQL::STRING_TYPE, + null: false, description: 'Web URL of the user.' - field :web_path, GraphQL::STRING_TYPE, null: false, + field :web_path, + type: GraphQL::STRING_TYPE, + null: false, description: 'Web path of the user.' - field :todos, Types::TodoType.connection_type, null: false, + field :todos, resolver: Resolvers::TodoResolver, description: 'To-do items of the user.' - field :group_memberships, Types::GroupMemberType.connection_type, null: true, + field :group_memberships, + type: Types::GroupMemberType.connection_type, + null: true, description: 'Group memberships of the user.' - field :group_count, GraphQL::INT_TYPE, null: true, + field :group_count, resolver: Resolvers::Users::GroupCountResolver, description: 'Group count for the user.', feature_flag: :user_group_counts - field :status, Types::UserStatusType, null: true, - description: 'User status.' - field :location, ::GraphQL::STRING_TYPE, null: true, + field :status, + type: Types::UserStatusType, + null: true, + description: 'User status.' + field :location, + type: ::GraphQL::STRING_TYPE, + null: true, description: 'The location of the user.' - field :project_memberships, Types::ProjectMemberType.connection_type, null: true, + field :project_memberships, + type: Types::ProjectMemberType.connection_type, + null: true, description: 'Project memberships of the user.' - field :starred_projects, Types::ProjectType.connection_type, null: true, + field :starred_projects, description: 'Projects starred by the user.', resolver: Resolvers::UserStarredProjectsResolver # Merge request field: MRs can be authored, assigned, or assigned-for-review: field :authored_merge_requests, resolver: Resolvers::AuthoredMergeRequestsResolver, - description: 'Merge Requests authored by the user.' + description: 'Merge requests authored by the user.' field :assigned_merge_requests, resolver: Resolvers::AssignedMergeRequestsResolver, - description: 'Merge Requests assigned to the user.' + description: 'Merge requests assigned to the user.' field :review_requested_merge_requests, resolver: Resolvers::ReviewRequestedMergeRequestsResolver, - description: 'Merge Requests assigned to the user for review.' + description: 'Merge requests assigned to the user for review.' field :snippets, - Types::SnippetType.connection_type, - null: true, description: 'Snippets authored by the user.', resolver: Resolvers::Users::SnippetsResolver field :callouts, |