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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb16
-rw-r--r--lib/gitlab/graphql/board/issues_connection_extension.rb2
-rw-r--r--lib/gitlab/graphql/deprecation.rb28
-rw-r--r--lib/gitlab/graphql/generic_tracing.rb8
-rw-r--r--lib/gitlab/graphql/loaders/batch_model_loader.rb15
-rw-r--r--lib/gitlab/graphql/markdown_field.rb4
-rw-r--r--lib/gitlab/graphql/present/field_extension.rb1
-rw-r--r--lib/gitlab/graphql/query_analyzers/ast/logger_analyzer.rb88
-rw-r--r--lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb78
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb84
-rw-r--r--lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb62
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