186 lines
6.5 KiB
Ruby
186 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Types
|
|
class BaseField < GraphQL::Schema::Field
|
|
include GitlabStyleDeprecations
|
|
|
|
argument_class ::Types::BaseArgument
|
|
|
|
DEFAULT_COMPLEXITY = 1
|
|
|
|
attr_reader :deprecation, :doc_reference
|
|
|
|
def initialize(**kwargs, &block)
|
|
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
|
|
@doc_reference = kwargs.delete(:see)
|
|
@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[:_deprecated_feature_flag]
|
|
kwargs = check_feature_flag(kwargs)
|
|
@deprecation = gitlab_deprecation(kwargs)
|
|
after_connection_extensions = kwargs.delete(:late_extensions) || []
|
|
|
|
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
|
|
|
|
after_connection_extensions.each { extension _1 } if after_connection_extensions.any?
|
|
end
|
|
|
|
def may_call_gitaly?
|
|
@constant_complexity || @calls_gitaly
|
|
end
|
|
|
|
def requires_argument?
|
|
@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
|
|
|
|
# This gets called from the gem's `calculate_complexity` method, allowing us
|
|
# to ensure our complexity calculation is used even for connections.
|
|
# This code is actually a copy of the default case in `calculate_complexity`
|
|
# in `lib/graphql/schema/field.rb`
|
|
# (https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/schema/field.rb)
|
|
def complexity_for(child_complexity:, query:, lookahead:)
|
|
defined_complexity = complexity
|
|
|
|
case defined_complexity
|
|
when Proc
|
|
arguments = query.arguments_for(lookahead.ast_nodes.first, self)
|
|
|
|
if arguments.respond_to?(:keyword_arguments)
|
|
defined_complexity.call(query.context, arguments.keyword_arguments, child_complexity)
|
|
else
|
|
child_complexity
|
|
end
|
|
when Numeric
|
|
defined_complexity + child_complexity
|
|
else
|
|
raise("Invalid complexity: #{defined_complexity.inspect} on #{path} (#{inspect})")
|
|
end
|
|
end
|
|
|
|
def base_complexity
|
|
complexity = DEFAULT_COMPLEXITY
|
|
complexity += 1 if calls_gitaly?
|
|
complexity
|
|
end
|
|
|
|
def calls_gitaly?
|
|
@calls_gitaly
|
|
end
|
|
|
|
def constant_complexity?
|
|
@constant_complexity
|
|
end
|
|
|
|
def visible?(context)
|
|
return false if feature_flag.present? && !Feature.enabled?(feature_flag)
|
|
|
|
super
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :feature_flag
|
|
|
|
def field_authorized?(object, ctx)
|
|
object = object.node if object.is_a?(GraphQL::Pagination::Connection::Edge)
|
|
|
|
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)
|
|
message_parts = ["#{description} Available only when feature flag `#{key}` is enabled."]
|
|
|
|
message_parts << if Feature::Definition.has_definition?(key) && Feature::Definition.default_enabled?(key)
|
|
"This flag is enabled by default."
|
|
else
|
|
"This flag is disabled by default, because the feature is experimental and is subject to change without notice."
|
|
end
|
|
|
|
message_parts.join(' ')
|
|
end
|
|
|
|
def check_feature_flag(args)
|
|
ff = args.delete(:_deprecated_feature_flag)
|
|
return args unless ff.present?
|
|
|
|
args[:description] = feature_documentation_message(ff, args[:description])
|
|
|
|
args
|
|
end
|
|
|
|
def field_complexity(resolver_class, current)
|
|
return current if current.present? && current > 0
|
|
|
|
if resolver_class
|
|
field_resolver_complexity
|
|
else
|
|
base_complexity
|
|
end
|
|
end
|
|
|
|
def field_resolver_complexity
|
|
# Complexity can be either integer or proc. If proc is used then it's
|
|
# called when computing a query complexity and context and query
|
|
# arguments are available for computing complexity. For resolvers we use
|
|
# proc because we set complexity depending on arguments and number of
|
|
# items which can be loaded.
|
|
proc do |ctx, args, child_complexity|
|
|
# Resolvers may add extra complexity depending on used arguments
|
|
complexity = child_complexity + resolver&.try(
|
|
:resolver_complexity, args, child_complexity: child_complexity
|
|
).to_i
|
|
complexity += 1 if calls_gitaly?
|
|
complexity += complexity * connection_complexity_multiplier(ctx, args)
|
|
|
|
complexity.to_i
|
|
end
|
|
end
|
|
|
|
def connection_complexity_multiplier(ctx, args)
|
|
# Resolvers may add extra complexity depending on number of items being loaded.
|
|
return 0 unless connection?
|
|
|
|
page_size = max_page_size || ctx.schema.default_max_page_size
|
|
limit_value = [args[:first], args[:last], page_size].compact.min
|
|
multiplier = resolver&.try(:complexity_multiplier, args).to_f
|
|
limit_value * multiplier
|
|
end
|
|
end
|
|
end
|