graphql 1.8.3 → 1.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/argument.rb +1 -0
  4. data/lib/graphql/authorization.rb +81 -0
  5. data/lib/graphql/boolean_type.rb +0 -1
  6. data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +2 -1
  7. data/lib/graphql/execution/execute.rb +34 -10
  8. data/lib/graphql/execution/lazy.rb +5 -1
  9. data/lib/graphql/field.rb +7 -1
  10. data/lib/graphql/float_type.rb +0 -1
  11. data/lib/graphql/id_type.rb +0 -1
  12. data/lib/graphql/int_type.rb +0 -1
  13. data/lib/graphql/introspection/entry_points.rb +2 -2
  14. data/lib/graphql/object_type.rb +3 -3
  15. data/lib/graphql/query.rb +6 -0
  16. data/lib/graphql/query/arguments.rb +2 -0
  17. data/lib/graphql/query/context.rb +6 -0
  18. data/lib/graphql/query/variables.rb +7 -1
  19. data/lib/graphql/relay/connection_instrumentation.rb +2 -2
  20. data/lib/graphql/relay/connection_resolve.rb +7 -27
  21. data/lib/graphql/relay/connection_type.rb +1 -0
  22. data/lib/graphql/relay/edge_type.rb +1 -0
  23. data/lib/graphql/relay/edges_instrumentation.rb +9 -25
  24. data/lib/graphql/relay/mutation/instrumentation.rb +1 -2
  25. data/lib/graphql/relay/mutation/resolve.rb +2 -4
  26. data/lib/graphql/relay/node.rb +1 -6
  27. data/lib/graphql/relay/page_info.rb +1 -9
  28. data/lib/graphql/schema.rb +84 -11
  29. data/lib/graphql/schema/argument.rb +13 -0
  30. data/lib/graphql/schema/enum.rb +1 -1
  31. data/lib/graphql/schema/enum_value.rb +4 -0
  32. data/lib/graphql/schema/field.rb +44 -11
  33. data/lib/graphql/schema/interface.rb +20 -0
  34. data/lib/graphql/schema/introspection_system.rb +1 -1
  35. data/lib/graphql/schema/member/base_dsl_methods.rb +25 -3
  36. data/lib/graphql/schema/member/instrumentation.rb +15 -17
  37. data/lib/graphql/schema/mutation.rb +4 -0
  38. data/lib/graphql/schema/object.rb +33 -0
  39. data/lib/graphql/schema/possible_types.rb +2 -0
  40. data/lib/graphql/schema/resolver.rb +10 -0
  41. data/lib/graphql/schema/traversal.rb +9 -2
  42. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +11 -2
  43. data/lib/graphql/string_type.rb +0 -1
  44. data/lib/graphql/types.rb +7 -0
  45. data/lib/graphql/types/relay.rb +31 -0
  46. data/lib/graphql/types/relay/base_connection.rb +87 -0
  47. data/lib/graphql/types/relay/base_edge.rb +51 -0
  48. data/lib/graphql/types/relay/base_field.rb +22 -0
  49. data/lib/graphql/types/relay/base_interface.rb +29 -0
  50. data/lib/graphql/types/relay/base_object.rb +26 -0
  51. data/lib/graphql/types/relay/node.rb +18 -0
  52. data/lib/graphql/types/relay/page_info.rb +23 -0
  53. data/lib/graphql/unauthorized_error.rb +20 -0
  54. data/lib/graphql/version.rb +1 -1
  55. data/spec/graphql/authorization_spec.rb +684 -0
  56. data/spec/graphql/query/variables_spec.rb +20 -0
  57. data/spec/graphql/relay/connection_instrumentation_spec.rb +1 -1
  58. data/spec/graphql/schema/resolver_spec.rb +31 -0
  59. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +52 -0
  60. data/spec/support/dummy/schema.rb +16 -0
  61. data/spec/support/star_wars/schema.rb +28 -17
  62. metadata +15 -2
@@ -12,8 +12,7 @@ module GraphQL
12
12
  def self.instrument(type, field)
13
13
  if field.mutation.is_a?(GraphQL::Relay::Mutation) || (field.mutation.is_a?(Class) && field.mutation < GraphQL::Schema::RelayClassicMutation)
14
14
  new_resolve = Mutation::Resolve.new(field.mutation, field.resolve_proc)
15
- new_lazy_resolve = Mutation::Resolve.new(field.mutation, field.lazy_resolve_proc)
16
- field.redefine(resolve: new_resolve, lazy_resolve: new_lazy_resolve)
15
+ field.redefine(resolve: new_resolve)
17
16
  else
18
17
  field
19
18
  end
@@ -21,10 +21,8 @@ module GraphQL
21
21
  err
22
22
  end
23
23
 
24
- if ctx.schema.lazy?(mutation_result)
25
- mutation_result
26
- else
27
- build_result(mutation_result, args, ctx)
24
+ ctx.schema.after_lazy(mutation_result) do |res|
25
+ build_result(res, args, ctx)
28
26
  end
29
27
  end
30
28
 
@@ -41,12 +41,7 @@ module GraphQL
41
41
 
42
42
  # @return [GraphQL::InterfaceType] The interface which all Relay types must implement
43
43
  def self.interface
44
- @interface ||= GraphQL::InterfaceType.define do
45
- name("Node")
46
- description("An object with an ID.")
47
- field(:id, types.ID.to_non_null_type, "ID of the object.")
48
- default_relay(true)
49
- end
44
+ @interface ||= GraphQL::Types::Relay::Node.graphql_definition
50
45
  end
51
46
 
52
47
  # A field resolve for finding objects by IDs
@@ -2,14 +2,6 @@
2
2
  module GraphQL
3
3
  module Relay
4
4
  # Wrap a Connection and expose its page info
5
- PageInfo = GraphQL::ObjectType.define do
6
- name("PageInfo")
7
- description("Information about pagination in a connection.")
8
- field :hasNextPage, !types.Boolean, "When paginating forwards, are there more items?", property: :has_next_page
9
- field :hasPreviousPage, !types.Boolean, "When paginating backwards, are there more items?", property: :has_previous_page
10
- field :startCursor, types.String, "When paginating backwards, the cursor to continue.", property: :start_cursor
11
- field :endCursor, types.String, "When paginating forwards, the cursor to continue.", property: :end_cursor
12
- default_relay true
13
- end
5
+ PageInfo = GraphQL::Types::Relay::PageInfo.graphql_definition
14
6
  end
15
7
  end
@@ -69,6 +69,7 @@ module GraphQL
69
69
  # end
70
70
  #
71
71
  class Schema
72
+ extend Forwardable
72
73
  extend GraphQL::Schema::Member::AcceptsDefinition
73
74
  include GraphQL::Define::InstanceDefinable
74
75
  accepts_definitions \
@@ -155,7 +156,7 @@ module GraphQL
155
156
  @parse_error_proc = DefaultParseError
156
157
  @instrumenters = Hash.new { |h, k| h[k] = [] }
157
158
  @lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
158
- @lazy_methods.set(GraphQL::Relay::ConnectionResolve::LazyNodesWrapper, :never_called)
159
+ @lazy_methods.set(GraphQL::Execution::Lazy, :value)
159
160
  @cursor_encoder = Base64Encoder
160
161
  # Default to the built-in execution strategy:
161
162
  @query_execution_strategy = self.class.default_execution_strategy
@@ -534,6 +535,10 @@ module GraphQL
534
535
  @type_error_proc = new_proc
535
536
  end
536
537
 
538
+ # Can't delegate to `class`
539
+ alias :_schema_class :class
540
+ def_delegators :_schema_class, :visible?, :accessible?, :authorized?, :unauthorized_object, :inaccessible_fields
541
+
537
542
  # A function to call when {#execute} receives an invalid query string
538
543
  #
539
544
  # @see {DefaultParseError} is the default behavior.
@@ -647,7 +652,7 @@ module GraphQL
647
652
  :static_validator, :introspection_system,
648
653
  :query_analyzers, :middleware, :tracers, :instrumenters,
649
654
  :query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
650
- :validate, :multiplex_analyzers, :lazy?, :lazy_method_name,
655
+ :validate, :multiplex_analyzers, :lazy?, :lazy_method_name, :after_lazy,
651
656
  # Configuration
652
657
  :max_complexity=, :max_depth=,
653
658
  :metadata,
@@ -696,6 +701,7 @@ module GraphQL
696
701
  schema_defn.cursor_encoder = cursor_encoder
697
702
  schema_defn.tracers.concat(defined_tracers)
698
703
  schema_defn.query_analyzers.concat(defined_query_analyzers)
704
+ schema_defn.query_analyzers << GraphQL::Authorization::Analyzer
699
705
  schema_defn.middleware.concat(defined_middleware)
700
706
  schema_defn.multiplex_analyzers.concat(defined_multiplex_analyzers)
701
707
  defined_instrumenters.each do |step, insts|
@@ -835,6 +841,41 @@ module GraphQL
835
841
  raise NotImplementedError, "#{self.name}.id_from_object(object, type, ctx) must be implemented to create global ids (tried to create an id for `#{object.inspect}`)"
836
842
  end
837
843
 
844
+ def visible?(member, context)
845
+ call_on_type_class(member, :visible?, context, default: true)
846
+ end
847
+
848
+ def accessible?(member, context)
849
+ call_on_type_class(member, :accessible?, context, default: true)
850
+ end
851
+
852
+ # This hook is called when a client tries to access one or more
853
+ # fields that fail the `accessible?` check.
854
+ #
855
+ # By default, an error is added to the response. Override this hook to
856
+ # track metrics or return a different error to the client.
857
+ #
858
+ # @param error [InaccessibleFieldsError] The analysis error for this check
859
+ # @return [AnalysisError, nil] Return an error to skip the query
860
+ def inaccessible_fields(error)
861
+ error
862
+ end
863
+
864
+ # This hook is called when an object fails an `authorized?` check.
865
+ # You might report to your bug tracker here, so you can correct
866
+ # the field resolvers not to return unauthorized objects.
867
+ #
868
+ # By default, this hook just replaces the unauthorized object with `nil`.
869
+ #
870
+ # If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
871
+ # in this hook.
872
+ #
873
+ # @param unauthorized_error [GraphQL::UnauthorizedError]
874
+ # @return [Object] The returned object will be put in the GraphQL response
875
+ def unauthorized_object(unauthorized_error)
876
+ nil
877
+ end
878
+
838
879
  def type_error(type_err, ctx)
839
880
  DefaultTypeError.call(type_err, ctx)
840
881
  end
@@ -905,6 +946,29 @@ module GraphQL
905
946
  def defined_multiplex_analyzers
906
947
  @defined_multiplex_analyzers ||= []
907
948
  end
949
+
950
+ # Given this schema member, find the class-based definition object
951
+ # whose `method_name` should be treated as an application hook
952
+ # @see {.visible?}
953
+ # @see {.accessible?}
954
+ # @see {.authorized?}
955
+ def call_on_type_class(member, method_name, *args, default:)
956
+ member = if member.respond_to?(:metadata)
957
+ member.metadata[:type_class] || member
958
+ else
959
+ member
960
+ end
961
+
962
+ if member.respond_to?(:relay_node_type) && (t = member.relay_node_type)
963
+ member = t
964
+ end
965
+
966
+ if member.respond_to?(method_name)
967
+ member.public_send(method_name, *args)
968
+ else
969
+ default
970
+ end
971
+ end
908
972
  end
909
973
 
910
974
 
@@ -923,6 +987,24 @@ module GraphQL
923
987
  end
924
988
  end
925
989
 
990
+ # Call the given block at the right time, either:
991
+ # - Right away, if `value` is not registered with `lazy_resolve`
992
+ # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
993
+ # @api private
994
+ def after_lazy(value)
995
+ if (lazy_method = lazy_method_name(value))
996
+ GraphQL::Execution::Lazy.new do
997
+ result = value.public_send(lazy_method)
998
+ # The returned result might also be lazy, so check it, too
999
+ after_lazy(result) do |final_result|
1000
+ yield(final_result)
1001
+ end
1002
+ end
1003
+ else
1004
+ yield(value)
1005
+ end
1006
+ end
1007
+
926
1008
  protected
927
1009
 
928
1010
  def rescues?
@@ -937,15 +1019,6 @@ module GraphQL
937
1019
 
938
1020
  private
939
1021
 
940
- # Wrap Relay-related objects in wrappers
941
- # @api private
942
- BUILT_IN_INSTRUMENTERS = [
943
- GraphQL::Relay::ConnectionInstrumentation,
944
- GraphQL::Relay::EdgesInstrumentation,
945
- GraphQL::Relay::Mutation::Instrumentation,
946
- GraphQL::Schema::Member::Instrumentation,
947
- ]
948
-
949
1022
  def rebuild_artifacts
950
1023
  if @rebuilding_artifacts
951
1024
  raise CyclicalDefinitionError, "Part of the schema build process re-triggered the schema build process, causing an infinite loop. Avoid using Schema#types, Schema#possible_types, and Schema#get_field during schema build."
@@ -9,6 +9,7 @@ module GraphQL
9
9
 
10
10
  # @return [String] the GraphQL name for this argument, camelized unless `camelize: false` is provided
11
11
  attr_reader :name
12
+ alias :graphql_name :name
12
13
 
13
14
  # @return [GraphQL::Schema::Field, Class] The field or input object this argument belongs to
14
15
  attr_reader :owner
@@ -53,6 +54,18 @@ module GraphQL
53
54
  end
54
55
  end
55
56
 
57
+ def visible?(context)
58
+ true
59
+ end
60
+
61
+ def accessible?(context)
62
+ true
63
+ end
64
+
65
+ def authorized?(obj, ctx)
66
+ true
67
+ end
68
+
56
69
  def to_graphql
57
70
  argument = GraphQL::Argument.new
58
71
  argument.name = @name
@@ -24,7 +24,7 @@ module GraphQL
24
24
 
25
25
  class << self
26
26
  extend Forwardable
27
- def_delegators :graphql_definition, :coerce_isolated_input, :coerce_isolated_result
27
+ def_delegators :graphql_definition, :coerce_isolated_input, :coerce_isolated_result, :coerce_input, :coerce_result
28
28
 
29
29
  # Define a value for this enum
30
30
  # @param graphql_name [String, Symbol] the GraphQL value for this, usually `SCREAMING_CASE`
@@ -69,6 +69,10 @@ module GraphQL
69
69
  enum_value.metadata[:type_class] = self
70
70
  enum_value
71
71
  end
72
+
73
+ def visible?(_ctx); true; end
74
+ def accessible?(_ctx); true; end
75
+ def authorized?(_ctx); true; end
72
76
  end
73
77
  end
74
78
  end
@@ -8,7 +8,8 @@ module GraphQL
8
8
  include GraphQL::Schema::Member::HasArguments
9
9
 
10
10
  # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided
11
- attr_accessor :name
11
+ attr_reader :name
12
+ alias :graphql_name :name
12
13
 
13
14
  # @return [String]
14
15
  attr_accessor :description
@@ -274,20 +275,52 @@ module GraphQL
274
275
  raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: #{$!.message}", $!.backtrace
275
276
  end
276
277
 
278
+ def visible?(context)
279
+ if @resolver_class
280
+ @resolver_class.visible?(context)
281
+ else
282
+ true
283
+ end
284
+ end
285
+
286
+ def accessible?(context)
287
+ if @resolver_class
288
+ @resolver_class.accessible?(context)
289
+ else
290
+ true
291
+ end
292
+ end
293
+
294
+ def authorized?(object, context)
295
+ if @resolver_class
296
+ @resolver_class.authorized?(object, context)
297
+ else
298
+ true
299
+ end
300
+ end
301
+
277
302
  # Implement {GraphQL::Field}'s resolve API.
278
303
  #
279
304
  # Eventually, we might hook up field instances to execution in another way. TBD.
280
305
  def resolve_field(obj, args, ctx)
281
- if @resolve_proc
282
- # Might be nil, still want to call the func in that case
283
- inner_obj = obj && obj.object
284
- @resolve_proc.call(inner_obj, args, ctx)
285
- elsif @resolver_class
286
- inner_obj = obj && obj.object
287
- singleton_inst = @resolver_class.new(object: inner_obj, context: ctx.query.context)
288
- public_send_field(singleton_inst, args, ctx)
289
- else
290
- public_send_field(obj, args, ctx)
306
+ ctx.schema.after_lazy(obj) do |after_obj|
307
+ # First, apply auth ...
308
+ query_ctx = ctx.query.context
309
+ inner_obj = after_obj && after_obj.object
310
+ if authorized?(inner_obj, query_ctx) && arguments.each_value.all? { |a| a.authorized?(inner_obj, query_ctx) }
311
+ # Then if it passed, resolve the field
312
+ if @resolve_proc
313
+ # Might be nil, still want to call the func in that case
314
+ @resolve_proc.call(inner_obj, args, ctx)
315
+ elsif @resolver_class
316
+ singleton_inst = @resolver_class.new(object: inner_obj, context: query_ctx)
317
+ public_send_field(singleton_inst, args, ctx)
318
+ else
319
+ public_send_field(after_obj, args, ctx)
320
+ end
321
+ else
322
+ nil
323
+ end
291
324
  end
292
325
  end
293
326
 
@@ -17,6 +17,26 @@ module GraphQL
17
17
  self::DefinitionMethods.module_eval(&block)
18
18
  end
19
19
 
20
+ # The interface is visible if any of its possible types are visible
21
+ def visible?(context)
22
+ context.schema.possible_types(self).each do |type|
23
+ if context.schema.visible?(type, context)
24
+ return true
25
+ end
26
+ end
27
+ false
28
+ end
29
+
30
+ # The interface is accessible if any of its possible types are accessible
31
+ def accessible?(context)
32
+ context.schema.possible_types(self).each do |type|
33
+ if context.schema.accessible?(type, context)
34
+ return true
35
+ end
36
+ end
37
+ false
38
+ end
39
+
20
40
  # Here's the tricky part. Make sure behavior keeps making its way down the inheritance chain.
21
41
  def included(child_class)
22
42
  if !child_class.is_a?(Class)
@@ -84,7 +84,7 @@ module GraphQL
84
84
  if obj.is_a?(GraphQL::Schema::Object)
85
85
  obj = obj.object
86
86
  end
87
- wrapped_object = @object_class.new(obj, query_ctx)
87
+ wrapped_object = @object_class.authorized_new(obj, query_ctx)
88
88
  @inner_resolve.call(wrapped_object, args, ctx)
89
89
  end
90
90
  end
@@ -22,7 +22,7 @@ module GraphQL
22
22
  overridden
23
23
  else # Fallback to Ruby constant name
24
24
  raise NotImplementedError, 'Anonymous class should declare an `graphql_name`' if name.nil?
25
-
25
+
26
26
  name.split("::").last.sub(/Type\Z/, "")
27
27
  end
28
28
  end
@@ -72,12 +72,34 @@ module GraphQL
72
72
  raise NotImplementedError
73
73
  end
74
74
 
75
+ alias :unwrap :itself
76
+
75
77
  def overridden_graphql_name
76
78
  @graphql_name || find_inherited_method(:overridden_graphql_name, nil)
77
79
  end
78
80
 
79
- def unwrap
80
- self
81
+ def visible?(context)
82
+ if @mutation
83
+ @mutation.visible?(context)
84
+ else
85
+ true
86
+ end
87
+ end
88
+
89
+ def accessible?(context)
90
+ if @mutation
91
+ @mutation.accessible?(context)
92
+ else
93
+ true
94
+ end
95
+ end
96
+
97
+ def authorized?(object, context)
98
+ if @mutation
99
+ @mutation.authorized?(object, context)
100
+ else
101
+ true
102
+ end
81
103
  end
82
104
 
83
105
  private
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # If it has a wrapper, apply it
28
28
  wrapper_class = root_type.metadata[:type_class]
29
29
  if wrapper_class
30
- new_root_value = wrapper_class.new(query.root_value, query.context)
30
+ new_root_value = wrapper_class.authorized_new(query.root_value, query.context)
31
31
  query.root_value = new_root_value
32
32
  end
33
33
  end
@@ -72,39 +72,37 @@ module GraphQL
72
72
 
73
73
  def call(obj, args, ctx)
74
74
  result = @inner_resolve.call(obj, args, ctx)
75
- if ctx.schema.lazy?(result)
76
- # Wrap it later
77
- result
78
- elsif ctx.skip == result
75
+ if ctx.skip == result || ctx.schema.lazy?(result) || result.nil? || result.is_a?(GraphQL::ExecutionError) || ctx.wrapped_object
79
76
  result
80
77
  else
81
- proxy_to_depth(result, @list_depth, @inner_return_type, ctx)
78
+ ctx.wrapped_object = true
79
+ proxy_to_depth(result, @list_depth, ctx)
82
80
  end
81
+ rescue GraphQL::UnauthorizedError => err
82
+ ctx.schema.unauthorized_object(err)
83
83
  end
84
84
 
85
85
  private
86
86
 
87
- def proxy_to_depth(obj, depth, type, ctx)
88
- if obj.nil?
89
- obj
90
- elsif depth > 0
91
- obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
87
+ def proxy_to_depth(inner_obj, depth, ctx)
88
+ if depth > 0
89
+ inner_obj.map { |i| proxy_to_depth(i, depth - 1, ctx) }
92
90
  else
93
- concrete_type = case type
91
+ concrete_type = case @inner_return_type
94
92
  when GraphQL::UnionType, GraphQL::InterfaceType
95
- ctx.query.resolve_type(type, obj)
93
+ ctx.query.resolve_type(@inner_return_type, inner_obj)
96
94
  when GraphQL::ObjectType
97
- type
95
+ @inner_return_type
98
96
  else
99
- raise "unexpected proxying type #{type} for #{obj} at #{ctx.owner_type}.#{ctx.field.name}"
97
+ raise "unexpected proxying type #{@inner_return_type} for #{inner_obj} at #{ctx.owner_type}.#{ctx.field.name}"
100
98
  end
101
99
 
102
100
  if concrete_type && (object_class = concrete_type.metadata[:type_class])
103
101
  # use the query-level context here, since it won't be field-specific anyways
104
102
  query_ctx = ctx.query.context
105
- object_class.new(obj, query_ctx)
103
+ object_class.authorized_new(inner_obj, query_ctx)
106
104
  else
107
- obj
105
+ inner_obj
108
106
  end
109
107
  end
110
108
  end