diff options
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r-- | lib/gitlab/graphql/authorize/authorize_resource.rb | 16 | ||||
-rw-r--r-- | lib/gitlab/graphql/board/issues_connection_extension.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/graphql/deprecation.rb | 28 | ||||
-rw-r--r-- | lib/gitlab/graphql/generic_tracing.rb | 8 | ||||
-rw-r--r-- | lib/gitlab/graphql/loaders/batch_model_loader.rb | 15 | ||||
-rw-r--r-- | lib/gitlab/graphql/markdown_field.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/graphql/present/field_extension.rb | 1 | ||||
-rw-r--r-- | lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb | 88 | ||||
-rw-r--r-- | lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb | 78 | ||||
-rw-r--r-- | lib/gitlab/graphql/query_analyzers/logger_analyzer.rb | 84 | ||||
-rw-r--r-- | lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb | 62 |
11 files changed, 221 insertions, 165 deletions
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb index dc49c806398..884fc85c4ec 100644 --- a/lib/gitlab/graphql/authorize/authorize_resource.rb +++ b/lib/gitlab/graphql/authorize/authorize_resource.rb @@ -15,11 +15,7 @@ module Gitlab # If the `#authorize` call is used on multiple classes, we add the # permissions specified on a subclass, to the ones that were specified # on its superclass. - @required_permissions ||= if respond_to?(:superclass) && superclass.respond_to?(:required_permissions) - superclass.required_permissions.dup - else - [] - end + @required_permissions ||= call_superclass_method(:required_permissions, []).dup end def authorize(*permissions) @@ -27,6 +23,8 @@ module Gitlab end def authorizes_object? + return true if call_superclass_method(:authorizes_object?, false) + defined?(@authorizes_object) ? @authorizes_object : false end @@ -37,6 +35,14 @@ module Gitlab def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR) raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, msg end + + private + + def call_superclass_method(method_name, or_else) + return or_else unless respond_to?(:superclass) && superclass.respond_to?(method_name) + + superclass.send(method_name) # rubocop: disable GitlabSecurity/PublicSend + end end def find_object(*args) diff --git a/lib/gitlab/graphql/board/issues_connection_extension.rb b/lib/gitlab/graphql/board/issues_connection_extension.rb index 9dcd8c92592..b909cb021de 100644 --- a/lib/gitlab/graphql/board/issues_connection_extension.rb +++ b/lib/gitlab/graphql/board/issues_connection_extension.rb @@ -2,7 +2,7 @@ module Gitlab module Graphql module Board - class IssuesConnectionExtension < GraphQL::Schema::Field::ConnectionExtension + class IssuesConnectionExtension < GraphQL::Schema::FieldExtension def after_resolve(value:, object:, context:, **rest) ::Boards::Issues::ListService .initialize_relative_positions(object.list.board, context[:current_user], value.nodes) diff --git a/lib/gitlab/graphql/deprecation.rb b/lib/gitlab/graphql/deprecation.rb index 3335e511714..d30751fe46e 100644 --- a/lib/gitlab/graphql/deprecation.rb +++ b/lib/gitlab/graphql/deprecation.rb @@ -3,9 +3,12 @@ module Gitlab module Graphql class Deprecation + REASON_RENAMED = :renamed + REASON_ALPHA = :alpha + REASONS = { - renamed: 'This was renamed.', - alpha: 'This feature is in Alpha, and can be removed or changed at any point.' + REASON_RENAMED => 'This was renamed.', + REASON_ALPHA => 'This feature is in Alpha. It can be changed or removed at any time.' }.freeze include ActiveModel::Validations @@ -39,7 +42,7 @@ module Gitlab def markdown(context: :inline) parts = [ - "#{deprecated_in(format: :markdown)}.", + "#{changed_in_milestone(format: :markdown)}.", reason_text, replacement_markdown.then { |r| "Use: #{r}." if r } ].compact @@ -77,7 +80,7 @@ module Gitlab [ reason_text, replacement && "Please use `#{replacement}`.", - "#{deprecated_in}." + "#{changed_in_milestone}." ].compact.join(' ') end @@ -107,15 +110,24 @@ module Gitlab end def description_suffix - " #{deprecated_in}: #{reason_text}" + " #{changed_in_milestone}: #{reason_text}" end - def deprecated_in(format: :plain) + # Returns 'Deprecated in <milestone>' for proper deprecations. + # Retruns 'Introduced in <milestone>' for :alpha deprecations. + # Formatted to markdown or plain format. + def changed_in_milestone(format: :plain) + verb = if reason == REASON_ALPHA + 'Introduced' + else + 'Deprecated' + end + case format when :plain - "Deprecated in #{milestone}" + "#{verb} in #{milestone}" when :markdown - "**Deprecated** in #{milestone}" + "**#{verb}** in #{milestone}" end end end diff --git a/lib/gitlab/graphql/generic_tracing.rb b/lib/gitlab/graphql/generic_tracing.rb index 936b22d5afa..d3de9c714f4 100644 --- a/lib/gitlab/graphql/generic_tracing.rb +++ b/lib/gitlab/graphql/generic_tracing.rb @@ -23,6 +23,14 @@ module Gitlab "#{type.name}.#{field.name}" end + def platform_authorized_key(type) + "#{type.graphql_name}.authorized" + end + + def platform_resolve_type_key(type) + "#{type.graphql_name}.resolve_type" + end + def platform_trace(platform_key, key, data, &block) tags = { platform_key: platform_key, key: key } start = Gitlab::Metrics::System.monotonic_time diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb index 805864cdd4c..41c3af33909 100644 --- a/lib/gitlab/graphql/loaders/batch_model_loader.rb +++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb @@ -4,20 +4,27 @@ module Gitlab module Graphql module Loaders class BatchModelLoader - attr_reader :model_class, :model_id + attr_reader :model_class, :model_id, :preloads - def initialize(model_class, model_id) + def initialize(model_class, model_id, preloads = nil) @model_class = model_class @model_id = model_id + @preloads = preloads || [] end # rubocop: disable CodeReuse/ActiveRecord def find - BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args| + BatchLoader::GraphQL.for([model_id.to_i, preloads]).batch(key: model_class) do |for_params, loader, args| model = args[:key] + keys_by_id = for_params.group_by(&:first) + ids = for_params.map(&:first) + preloads = for_params.flat_map(&:second).uniq results = model.where(id: ids) + results = results.preload(*preloads) unless preloads.empty? - results.each { |record| loader.call(record.id, record) } + results.each do |record| + keys_by_id.fetch(record.id, []).each { |k| loader.call(k, record) } + end end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/graphql/markdown_field.rb b/lib/gitlab/graphql/markdown_field.rb index 6188d860aba..43dddf4c4bc 100644 --- a/lib/gitlab/graphql/markdown_field.rb +++ b/lib/gitlab/graphql/markdown_field.rb @@ -22,8 +22,10 @@ module Gitlab field name, GraphQL::Types::String, **kwargs define_method resolver_method do + markdown_object = block_given? ? yield(object) : object + # We need to `dup` the context so the MarkdownHelper doesn't modify it - ::MarkupHelper.markdown_field(object, method_name.to_sym, context.to_h.dup) + ::MarkupHelper.markdown_field(markdown_object, method_name.to_sym, context.to_h.dup) end end end diff --git a/lib/gitlab/graphql/present/field_extension.rb b/lib/gitlab/graphql/present/field_extension.rb index 050a3a276ea..bc6d0c6fd35 100644 --- a/lib/gitlab/graphql/present/field_extension.rb +++ b/lib/gitlab/graphql/present/field_extension.rb @@ -21,6 +21,7 @@ module Gitlab # TODO: remove this when resolve procs are removed from the # graphql-ruby library, and all field instrumentation is removed. # See: https://github.com/rmosolgo/graphql-ruby/issues/3385 + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363131 presented = field.owner.try(:present, object, attrs) || object yield(presented, arguments) end diff --git a/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb new file mode 100644 index 00000000000..9a7069249ec --- /dev/null +++ b/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module QueryAnalyzers + module AST + class LoggerAnalyzer < GraphQL::Analysis::AST::Analyzer + COMPLEXITY_ANALYZER = GraphQL::Analysis::AST::QueryComplexity + DEPTH_ANALYZER = GraphQL::Analysis::AST::QueryDepth + FIELD_USAGE_ANALYZER = GraphQL::Analysis::AST::FieldUsage + ALL_ANALYZERS = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER, FIELD_USAGE_ANALYZER].freeze + + def initialize(query) + super + + @results = default_initial_values(query).merge({ + time_started: Gitlab::Metrics::System.monotonic_time + }) + rescue StandardError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) + @results = default_initial_values(query_or_multiplex) + end + + def result + complexity, depth, field_usages = + GraphQL::Analysis::AST.analyze_query(@subject, ALL_ANALYZERS, multiplex_analyzers: []) + + results[:depth] = depth + results[:complexity] = complexity + # This duration is not the execution time of the + # query but the execution time of the analyzer. + results[:duration_s] = duration(results[:time_started]) + results[:used_fields] = field_usages[:used_fields] + results[:used_deprecated_fields] = field_usages[:used_deprecated_fields] + + push_to_request_store(results) + + # This gl_analysis is included in the tracer log + query.context[:gl_analysis] = results.except!(:time_started, :query) + rescue StandardError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) + end + + private + + attr_reader :results + + def push_to_request_store(results) + query = @subject + + # TODO: This RequestStore management is used to handle setting request wide metadata + # to improve preexisting logging. We should handle this either with ApplicationContext + # or in a separate tracer. + # https://gitlab.com/gitlab-org/gitlab/-/issues/343802 + + RequestStore.store[:graphql_logs] ||= [] + RequestStore.store[:graphql_logs] << results.except(:time_started, :duration_s).merge({ + variables: process_variables(query.provided_variables), + operation_name: query.operation_name + }) + end + + def process_variables(variables) + filtered_variables = filter_sensitive_variables(variables) + filtered_variables.try(:to_s) || filtered_variables + end + + def filter_sensitive_variables(variables) + ActiveSupport::ParameterFilter + .new(::Rails.application.config.filter_parameters) + .filter(variables) + end + + def duration(time_started) + Gitlab::Metrics::System.monotonic_time - time_started + end + + def default_initial_values(query) + { + time_started: Gitlab::Metrics::System.monotonic_time, + duration_s: nil + } + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb new file mode 100644 index 00000000000..4e90e4c912f --- /dev/null +++ b/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# Recursive queries, with relatively low effort, can quickly spiral out of control exponentially +# and may not be picked up by depth and complexity alone. +module Gitlab + module Graphql + module QueryAnalyzers + module AST + class RecursionAnalyzer < GraphQL::Analysis::AST::Analyzer + IGNORED_FIELDS = %w(node edges nodes ofType).freeze + RECURSION_THRESHOLD = 2 + + def initialize(query) + super + + @node_visits = {} + @recurring_fields = {} + end + + def on_enter_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] += 1 + + times_encountered = @node_visits[node_name] + recurring_fields[node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered) + end + + # Visitors are all defined on the AST::Analyzer base class + # We override them for custom analyzers. + def on_leave_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] -= 1 + end + + def result + @recurring_fields = @recurring_fields.select { |k, v| recursion_too_deep?(k, v) } + + if @recurring_fields.any? + GraphQL::AnalysisError.new(<<~MSG) + Recursive query - too many of fields '#{@recurring_fields}' detected + in single branch of the query") + MSG + end + end + + private + + attr_reader :node_visits, :recurring_fields + + def recursion_too_deep?(node_name, times_encountered) + return if IGNORED_FIELDS.include?(node_name) + + times_encountered > recursion_threshold + end + + def skip_node?(node, visitor) + # We don't want to count skipped fields or fields + # inside fragment definitions + return false if visitor.skipping? || visitor.visiting_fragment_definition? + + !node.is_a?(GraphQL::Language::Nodes::Field) || node.selections.empty? + end + + # separated into a method for use in allow_high_graphql_recursion + def recursion_threshold + RECURSION_THRESHOLD + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb deleted file mode 100644 index 207324e73bd..00000000000 --- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module QueryAnalyzers - class LoggerAnalyzer - COMPLEXITY_ANALYZER = GraphQL::Analysis::QueryComplexity.new { |query, complexity_value| complexity_value } - DEPTH_ANALYZER = GraphQL::Analysis::QueryDepth.new { |query, depth_value| depth_value } - FIELD_USAGE_ANALYZER = GraphQL::Analysis::FieldUsage.new { |query, used_fields, used_deprecated_fields| [used_fields, used_deprecated_fields] } - ALL_ANALYZERS = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER, FIELD_USAGE_ANALYZER].freeze - - def initial_value(query) - { - time_started: Gitlab::Metrics::System.monotonic_time, - query: query - } - end - - def call(memo, *) - memo - end - - def final_value(memo) - return if memo.nil? - - query = memo[:query] - complexity, depth, field_usages = GraphQL::Analysis.analyze_query(query, ALL_ANALYZERS) - - memo[:depth] = depth - memo[:complexity] = complexity - # This duration is not the execution time of the - # query but the execution time of the analyzer. - memo[:duration_s] = duration(memo[:time_started]) - memo[:used_fields] = field_usages.first - memo[:used_deprecated_fields] = field_usages.second - - push_to_request_store(memo) - - # This gl_analysis is included in the tracer log - query.context[:gl_analysis] = memo.except!(:time_started, :query) - rescue StandardError => e - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) - end - - private - - def push_to_request_store(memo) - query = memo[:query] - - # TODO: This RequestStore management is used to handle setting request wide metadata - # to improve preexisting logging. We should handle this either with ApplicationContext - # or in a separate tracer. - # https://gitlab.com/gitlab-org/gitlab/-/issues/343802 - - RequestStore.store[:graphql_logs] ||= [] - RequestStore.store[:graphql_logs] << memo.except(:time_started, :duration_s, :query).merge({ - variables: process_variables(query.provided_variables), - operation_name: query.operation_name - }) - end - - def process_variables(variables) - filtered_variables = filter_sensitive_variables(variables) - - if filtered_variables.respond_to?(:to_s) - filtered_variables.to_s - else - filtered_variables - end - end - - def filter_sensitive_variables(variables) - ActiveSupport::ParameterFilter - .new(::Rails.application.config.filter_parameters) - .filter(variables) - end - - def duration(time_started) - Gitlab::Metrics::System.monotonic_time - time_started - end - end - end - end -end diff --git a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb deleted file mode 100644 index 79a7104a2ff..00000000000 --- a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -# Recursive queries, with relatively low effort, can quickly spiral out of control exponentially -# and may not be picked up by depth and complexity alone. -module Gitlab - module Graphql - module QueryAnalyzers - class RecursionAnalyzer - IGNORED_FIELDS = %w(node edges nodes ofType).freeze - RECURSION_THRESHOLD = 2 - - def initial_value(query) - { - recurring_fields: {} - } - end - - def call(memo, visit_type, irep_node) - return memo if skip_node?(irep_node) - - node_name = irep_node.ast_node.name - times_encountered = memo[node_name] || 0 - - if visit_type == :enter - times_encountered += 1 - memo[:recurring_fields][node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered) - else - times_encountered -= 1 - end - - memo[node_name] = times_encountered - memo - end - - def final_value(memo) - recurring_fields = memo[:recurring_fields] - recurring_fields = recurring_fields.select { |k, v| recursion_too_deep?(k, v) } - if recurring_fields.any? - GraphQL::AnalysisError.new("Recursive query - too many of fields '#{recurring_fields}' detected in single branch of the query") - end - end - - private - - def recursion_too_deep?(node_name, times_encountered) - return if IGNORED_FIELDS.include?(node_name) - - times_encountered > recursion_threshold - end - - def skip_node?(irep_node) - ast_node = irep_node.ast_node - !ast_node.is_a?(GraphQL::Language::Nodes::Field) || ast_node.selections.empty? - end - - def recursion_threshold - RECURSION_THRESHOLD - end - end - end - end -end |