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
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class UnauthorizedError < GraphQL::Error
4
+ # @return [Object] the application object that failed the authorization check
5
+ attr_reader :object
6
+
7
+ # @return [Class] the GraphQL object type whose `.authorized?` method was called (and returned false)
8
+ attr_reader :type
9
+
10
+ # @return [GraphQL::Query::Context] the context for the current query
11
+ attr_reader :context
12
+
13
+ def initialize(object:, type:, context:)
14
+ @object = object
15
+ @type = type
16
+ @context = context
17
+ super("An instance of #{object.class} failed #{type.name}'s authorization check")
18
+ end
19
+ end
20
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.8.3"
3
+ VERSION = "1.8.4"
4
4
  end
@@ -0,0 +1,684 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::Authorization do
5
+ module AuthTest
6
+ class Box
7
+ attr_reader :value
8
+ def initialize(value:)
9
+ @value = value
10
+ end
11
+ end
12
+
13
+ class BaseArgument < GraphQL::Schema::Argument
14
+ def visible?(context)
15
+ super && (context[:hide] ? @name != "hidden" : true)
16
+ end
17
+
18
+ def accessible?(context)
19
+ super && (context[:hide] ? @name != "inaccessible" : true)
20
+ end
21
+
22
+ def authorized?(parent_object, context)
23
+ super && parent_object != :hide2
24
+ end
25
+ end
26
+
27
+ class BaseField < GraphQL::Schema::Field
28
+ def initialize(*args, edge_class: nil, **kwargs, &block)
29
+ @edge_class = edge_class
30
+ super(*args, **kwargs, &block)
31
+ end
32
+
33
+ def to_graphql
34
+ field_defn = super
35
+ if @edge_class
36
+ field_defn.edge_class = @edge_class
37
+ end
38
+ field_defn
39
+ end
40
+
41
+ argument_class BaseArgument
42
+ def visible?(context)
43
+ super && (context[:hide] ? @name != "hidden" : true)
44
+ end
45
+
46
+ def accessible?(context)
47
+ super && (context[:hide] ? @name != "inaccessible" : true)
48
+ end
49
+
50
+ def authorized?(object, context)
51
+ super && object != :hide
52
+ end
53
+ end
54
+
55
+ class BaseObject < GraphQL::Schema::Object
56
+ field_class BaseField
57
+ end
58
+
59
+ module BaseInterface
60
+ include GraphQL::Schema::Interface
61
+ end
62
+
63
+ class BaseEnumValue < GraphQL::Schema::EnumValue
64
+ def initialize(*args, role: nil, **kwargs)
65
+ @role = role
66
+ super(*args, **kwargs)
67
+ end
68
+
69
+ def visible?(context)
70
+ super && (context[:hide] ? @role != :hidden : true)
71
+ end
72
+ end
73
+
74
+ class BaseEnum < GraphQL::Schema::Enum
75
+ enum_value_class(BaseEnumValue)
76
+ end
77
+
78
+ module HiddenInterface
79
+ include BaseInterface
80
+
81
+ def self.visible?(ctx)
82
+ super && !ctx[:hide]
83
+ end
84
+
85
+ def self.resolve_type(obj, ctx)
86
+ HiddenObject
87
+ end
88
+ end
89
+
90
+ module HiddenDefaultInterface
91
+ include BaseInterface
92
+ # visible? will call the super method
93
+ def self.resolve_type(obj, ctx)
94
+ HiddenObject
95
+ end
96
+ end
97
+
98
+ class HiddenObject < BaseObject
99
+ implements HiddenInterface
100
+ implements HiddenDefaultInterface
101
+ def self.visible?(ctx)
102
+ super && !ctx[:hide]
103
+ end
104
+ end
105
+
106
+ class RelayObject < BaseObject
107
+ def self.visible?(ctx)
108
+ super && !ctx[:hidden_relay]
109
+ end
110
+
111
+ def self.accessible?(ctx)
112
+ super && !ctx[:inaccessible_relay]
113
+ end
114
+
115
+ def self.authorized?(_val, ctx)
116
+ super && !ctx[:unauthorized_relay]
117
+ end
118
+ end
119
+
120
+ # TODO test default behavior for abstract types,
121
+ # that they check their concrete types
122
+ module InaccessibleInterface
123
+ include BaseInterface
124
+
125
+ def self.accessible?(ctx)
126
+ super && !ctx[:hide]
127
+ end
128
+
129
+ def self.resolve_type(obj, ctx)
130
+ InaccessibleObject
131
+ end
132
+ end
133
+
134
+ module InaccessibleDefaultInterface
135
+ include BaseInterface
136
+ # accessible? will call the super method
137
+ def self.resolve_type(obj, ctx)
138
+ InaccessibleObject
139
+ end
140
+ end
141
+
142
+ class InaccessibleObject < BaseObject
143
+ implements InaccessibleInterface
144
+ implements InaccessibleDefaultInterface
145
+ def self.accessible?(ctx)
146
+ super && !ctx[:hide]
147
+ end
148
+ end
149
+
150
+ class UnauthorizedObject < BaseObject
151
+ def self.authorized?(value, context)
152
+ super && !context[:hide]
153
+ end
154
+ end
155
+
156
+ class UnauthorizedBox < BaseObject
157
+ # Hide `"a"`
158
+ def self.authorized?(value, context)
159
+ super && value != "a"
160
+ end
161
+
162
+ field :value, String, null: false, method: :object
163
+ end
164
+
165
+ module UnauthorizedInterface
166
+ include BaseInterface
167
+
168
+ def self.resolve_type(obj, ctx)
169
+ if obj.is_a?(String)
170
+ UnauthorizedCheckBox
171
+ else
172
+ raise "Unexpected value: #{obj.inspect}"
173
+ end
174
+ end
175
+ end
176
+
177
+ class UnauthorizedCheckBox < BaseObject
178
+ implements UnauthorizedInterface
179
+ # This authorized check returns a lazy object, it should be synced by the runtime.
180
+ def self.authorized?(value, context)
181
+ if !value.is_a?(String)
182
+ raise "Unexpected box value: #{value.inspect}"
183
+ end
184
+ is_authed = super && value != "a"
185
+ # Make it many levels nested just to make sure we support nested lazy objects
186
+ Box.new(value: Box.new(value: Box.new(value: Box.new(value: is_authed))))
187
+ end
188
+
189
+ field :value, String, null: false, method: :object
190
+ end
191
+
192
+ class IntegerObject < BaseObject
193
+ def self.authorized?(obj, ctx)
194
+ if !obj.is_a?(Integer)
195
+ raise "Unexpected IntegerObject: #{obj}"
196
+ end
197
+ is_allowed = !(ctx[:unauthorized_relay] || obj == ctx[:exclude_integer])
198
+ Box.new(value: Box.new(value: is_allowed))
199
+ end
200
+ field :value, Integer, null: false, method: :object
201
+ end
202
+
203
+ class IntegerObjectEdge < GraphQL::Types::Relay::BaseEdge
204
+ node_type(IntegerObject)
205
+ end
206
+
207
+ class IntegerObjectConnection < GraphQL::Types::Relay::BaseConnection
208
+ edge_type(IntegerObjectEdge)
209
+ end
210
+
211
+ class LandscapeFeature < BaseEnum
212
+ value "MOUNTAIN"
213
+ value "STREAM", role: :unauthorized
214
+ value "FIELD", role: :inaccessible
215
+ value "TAR_PIT", role: :hidden
216
+ end
217
+
218
+ class Query < BaseObject
219
+ field :hidden, Integer, null: false
220
+ field :unauthorized, Integer, null: true, method: :object
221
+ field :int2, Integer, null: true do
222
+ argument :int, Integer, required: false
223
+ argument :hidden, Integer, required: false
224
+ argument :inaccessible, Integer, required: false
225
+ argument :unauthorized, Integer, required: false
226
+ end
227
+
228
+ def int2(**args)
229
+ args[:unauthorized] || 1
230
+ end
231
+
232
+ field :landscape_feature, LandscapeFeature, null: false do
233
+ argument :string, String, required: false
234
+ argument :enum, LandscapeFeature, required: false
235
+ end
236
+
237
+ def landscape_feature(string: nil, enum: nil)
238
+ string || enum
239
+ end
240
+
241
+ field :landscape_features, [LandscapeFeature], null: false do
242
+ argument :strings, [String], required: false
243
+ argument :enums, [LandscapeFeature], required: false
244
+ end
245
+
246
+ def landscape_features(strings: [], enums: [])
247
+ strings + enums
248
+ end
249
+
250
+ def empty_array; []; end
251
+ field :hidden_object, HiddenObject, null: false, method: :itself
252
+ field :hidden_interface, HiddenInterface, null: false, method: :itself
253
+ field :hidden_default_interface, HiddenDefaultInterface, null: false, method: :itself
254
+ field :hidden_connection, RelayObject.connection_type, null: :false, method: :empty_array
255
+ field :hidden_edge, RelayObject.edge_type, null: :false, method: :itself
256
+
257
+ field :inaccessible, Integer, null: false, method: :object_id
258
+ field :inaccessible_object, InaccessibleObject, null: false, method: :itself
259
+ field :inaccessible_interface, InaccessibleInterface, null: false, method: :itself
260
+ field :inaccessible_default_interface, InaccessibleDefaultInterface, null: false, method: :itself
261
+ field :inaccessible_connection, RelayObject.connection_type, null: :false, method: :empty_array
262
+ field :inaccessible_edge, RelayObject.edge_type, null: :false, method: :itself
263
+
264
+ field :unauthorized_object, UnauthorizedObject, null: true, method: :itself
265
+ field :unauthorized_connection, RelayObject.connection_type, null: :false, method: :empty_array
266
+ field :unauthorized_edge, RelayObject.edge_type, null: :false, method: :itself
267
+ field :unauthorized_lazy_box, UnauthorizedBox, null: true do
268
+ argument :value, String, required: true
269
+ end
270
+ def unauthorized_lazy_box(value:)
271
+ # Make it extra nested, just for good measure.
272
+ Box.new(value: Box.new(value: value))
273
+ end
274
+ field :unauthorized_list_items, [UnauthorizedObject], null: true
275
+ def unauthorized_list_items
276
+ [self, self]
277
+ end
278
+
279
+ field :unauthorized_lazy_check_box, UnauthorizedCheckBox, null: true, method: :unauthorized_lazy_box do
280
+ argument :value, String, required: true
281
+ end
282
+
283
+ field :unauthorized_interface, UnauthorizedInterface, null: true, method: :unauthorized_lazy_box do
284
+ argument :value, String, required: true
285
+ end
286
+
287
+ field :integers, IntegerObjectConnection, null: false
288
+
289
+ def integers
290
+ [1,2,3]
291
+ end
292
+
293
+ field :lazy_integers, IntegerObjectConnection, null: false
294
+
295
+ def lazy_integers
296
+ Box.new(value: Box.new(value: [1,2,3]))
297
+ end
298
+ end
299
+
300
+ class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation
301
+ def self.visible?(ctx)
302
+ super && (ctx[:hidden_mutation] ? false : true)
303
+ end
304
+ end
305
+
306
+ class DoInaccessibleStuff < GraphQL::Schema::RelayClassicMutation
307
+ def self.accessible?(ctx)
308
+ super && (ctx[:inaccessible_mutation] ? false : true)
309
+ end
310
+ end
311
+
312
+ class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation
313
+ def self.authorized?(obj, ctx)
314
+ super && (ctx[:unauthorized_mutation] ? false : true)
315
+ end
316
+ end
317
+
318
+ class Mutation < BaseObject
319
+ field :do_hidden_stuff, mutation: DoHiddenStuff
320
+ field :do_inaccessible_stuff, mutation: DoInaccessibleStuff
321
+ field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff
322
+ end
323
+
324
+ class Schema < GraphQL::Schema
325
+ query(Query)
326
+ mutation(Mutation)
327
+
328
+ lazy_resolve(Box, :value)
329
+
330
+ def self.unauthorized_object(err)
331
+ raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}"
332
+ end
333
+
334
+ # use GraphQL::Backtrace
335
+ end
336
+ end
337
+
338
+ def auth_execute(*args)
339
+ AuthTest::Schema.execute(*args)
340
+ end
341
+
342
+ describe "applying the visible? method" do
343
+ it "works in queries" do
344
+ res = auth_execute(" { int int2 } ", context: { hide: true })
345
+ assert_equal 1, res["errors"].size
346
+ end
347
+
348
+ it "applies return type visibility to fields" do
349
+ error_queries = {
350
+ "hiddenObject" => "{ hiddenObject { __typename } }",
351
+ "hiddenInterface" => "{ hiddenInterface { __typename } }",
352
+ "hiddenDefaultInterface" => "{ hiddenDefaultInterface { __typename } }",
353
+ }
354
+
355
+ error_queries.each do |name, q|
356
+ hidden_res = auth_execute(q, context: { hide: true})
357
+ assert_equal ["Field '#{name}' doesn't exist on type 'Query'"], hidden_res["errors"].map { |e| e["message"] }
358
+
359
+ visible_res = auth_execute(q)
360
+ # Both fields exist; the interface resolves to the object type, though
361
+ assert_equal "HiddenObject", visible_res["data"][name]["__typename"]
362
+ end
363
+ end
364
+
365
+ it "uses the mutation for derived fields, inputs and outputs" do
366
+ query = "mutation { doHiddenStuff(input: {}) { __typename } }"
367
+ res = auth_execute(query, context: { hidden_mutation: true })
368
+ assert_equal ["Field 'doHiddenStuff' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] }
369
+
370
+ # `#resolve` isn't implemented, so this errors out:
371
+ assert_raises NotImplementedError do
372
+ auth_execute(query)
373
+ end
374
+
375
+ introspection_q = <<-GRAPHQL
376
+ {
377
+ t1: __type(name: "DoHiddenStuffInput") { name }
378
+ t2: __type(name: "DoHiddenStuffPayload") { name }
379
+ }
380
+ GRAPHQL
381
+ hidden_introspection_res = auth_execute(introspection_q, context: { hidden_mutation: true })
382
+ assert_nil hidden_introspection_res["data"]["t1"]
383
+ assert_nil hidden_introspection_res["data"]["t2"]
384
+
385
+ visible_introspection_res = auth_execute(introspection_q)
386
+ assert_equal "DoHiddenStuffInput", visible_introspection_res["data"]["t1"]["name"]
387
+ assert_equal "DoHiddenStuffPayload", visible_introspection_res["data"]["t2"]["name"]
388
+ end
389
+
390
+ it "uses the base type for edges and connections" do
391
+ query = <<-GRAPHQL
392
+ {
393
+ hiddenConnection { __typename }
394
+ hiddenEdge { __typename }
395
+ }
396
+ GRAPHQL
397
+
398
+ hidden_res = auth_execute(query, context: { hidden_relay: true })
399
+ assert_equal 2, hidden_res["errors"].size
400
+
401
+ visible_res = auth_execute(query)
402
+ assert_equal "RelayObjectConnection", visible_res["data"]["hiddenConnection"]["__typename"]
403
+ assert_equal "RelayObjectEdge", visible_res["data"]["hiddenEdge"]["__typename"]
404
+ end
405
+
406
+ it "treats hidden enum values as non-existant, even in lists" do
407
+ hidden_res_1 = auth_execute <<-GRAPHQL, context: { hide: true }
408
+ {
409
+ landscapeFeature(enum: TAR_PIT)
410
+ }
411
+ GRAPHQL
412
+
413
+ assert_equal ["Argument 'enum' on Field 'landscapeFeature' has an invalid value. Expected type 'LandscapeFeature'."], hidden_res_1["errors"].map { |e| e["message"] }
414
+
415
+ hidden_res_2 = auth_execute <<-GRAPHQL, context: { hide: true }
416
+ {
417
+ landscapeFeatures(enums: [STREAM, TAR_PIT])
418
+ }
419
+ GRAPHQL
420
+
421
+ assert_equal ["Argument 'enums' on Field 'landscapeFeatures' has an invalid value. Expected type '[LandscapeFeature!]'."], hidden_res_2["errors"].map { |e| e["message"] }
422
+
423
+ success_res = auth_execute <<-GRAPHQL, context: { hide: false }
424
+ {
425
+ landscapeFeature(enum: TAR_PIT)
426
+ landscapeFeatures(enums: [STREAM, TAR_PIT])
427
+ }
428
+ GRAPHQL
429
+
430
+ assert_equal "TAR_PIT", success_res["data"]["landscapeFeature"]
431
+ assert_equal ["STREAM", "TAR_PIT"], success_res["data"]["landscapeFeatures"]
432
+ end
433
+
434
+ it "refuses to resolve to hidden enum values" do
435
+ assert_raises(GraphQL::EnumType::UnresolvedValueError) do
436
+ auth_execute <<-GRAPHQL, context: { hide: true }
437
+ {
438
+ landscapeFeature(string: "TAR_PIT")
439
+ }
440
+ GRAPHQL
441
+ end
442
+
443
+ assert_raises(GraphQL::EnumType::UnresolvedValueError) do
444
+ auth_execute <<-GRAPHQL, context: { hide: true }
445
+ {
446
+ landscapeFeatures(strings: ["STREAM", "TAR_PIT"])
447
+ }
448
+ GRAPHQL
449
+ end
450
+ end
451
+
452
+ it "works in introspection" do
453
+ res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true }
454
+ {
455
+ query: __type(name: "Query") {
456
+ fields {
457
+ name
458
+ args { name }
459
+ }
460
+ }
461
+
462
+ hiddenObject: __type(name: "HiddenObject") { name }
463
+ hiddenInterface: __type(name: "HiddenInterface") { name }
464
+ landscapeFeatures: __type(name: "LandscapeFeature") { enumValues { name } }
465
+ }
466
+ GRAPHQL
467
+ query_field_names = res["data"]["query"]["fields"].map { |f| f["name"] }
468
+ refute_includes query_field_names, "int"
469
+ int2_arg_names = res["data"]["query"]["fields"].find { |f| f["name"] == "int2" }["args"].map { |a| a["name"] }
470
+ assert_equal ["int", "inaccessible", "unauthorized"], int2_arg_names
471
+
472
+ assert_nil res["data"]["hiddenObject"]
473
+ assert_nil res["data"]["hiddenInterface"]
474
+
475
+ visible_landscape_features = res["data"]["landscapeFeatures"]["enumValues"].map { |v| v["name"] }
476
+ assert_equal ["MOUNTAIN", "STREAM", "FIELD"], visible_landscape_features
477
+ end
478
+ end
479
+
480
+ describe "applying the accessible? method" do
481
+ it "works with fields and arguments" do
482
+ queries = {
483
+ "{ inaccessible }" => ["Some fields in this query are not accessible: inaccessible"],
484
+ "{ int2(inaccessible: 1) }" => ["Some fields in this query are not accessible: int2"],
485
+ }
486
+
487
+ queries.each do |query_str, errors|
488
+ res = auth_execute(query_str, context: { hide: true })
489
+ assert_equal errors, res.fetch("errors").map { |e| e["message"] }
490
+
491
+ res = auth_execute(query_str, context: { hide: false })
492
+ refute res.key?("errors")
493
+ end
494
+ end
495
+
496
+ it "works with return types" do
497
+ queries = {
498
+ "{ inaccessibleObject { __typename } }" => ["Some fields in this query are not accessible: inaccessibleObject"],
499
+ "{ inaccessibleInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleInterface"],
500
+ "{ inaccessibleDefaultInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleDefaultInterface"],
501
+ }
502
+
503
+ queries.each do |query_str, errors|
504
+ res = auth_execute(query_str, context: { hide: true })
505
+ assert_equal errors, res["errors"].map { |e| e["message"] }
506
+
507
+ res = auth_execute(query_str, context: { hide: false })
508
+ refute res.key?("errors")
509
+ end
510
+ end
511
+
512
+ it "works with mutations" do
513
+ query = "mutation { doInaccessibleStuff(input: {}) { __typename } }"
514
+ res = auth_execute(query, context: { inaccessible_mutation: true })
515
+ assert_equal ["Some fields in this query are not accessible: doInaccessibleStuff"], res["errors"].map { |e| e["message"] }
516
+
517
+ assert_raises NotImplementedError do
518
+ auth_execute(query)
519
+ end
520
+ end
521
+
522
+ it "works with edges and connections" do
523
+ query = <<-GRAPHQL
524
+ {
525
+ inaccessibleConnection { __typename }
526
+ inaccessibleEdge { __typename }
527
+ }
528
+ GRAPHQL
529
+
530
+ inaccessible_res = auth_execute(query, context: { inaccessible_relay: true })
531
+ assert_equal ["Some fields in this query are not accessible: inaccessibleConnection, inaccessibleEdge"], inaccessible_res["errors"].map { |e| e["message"] }
532
+
533
+ accessible_res = auth_execute(query)
534
+ refute accessible_res.key?("errors")
535
+ end
536
+ end
537
+
538
+ describe "applying the authorized? method" do
539
+ it "halts on unauthorized objects" do
540
+ query = "{ unauthorizedObject { __typename } }"
541
+ hidden_response = auth_execute(query, context: { hide: true })
542
+ assert_nil hidden_response["data"].fetch("unauthorizedObject")
543
+ visible_response = auth_execute(query, context: {})
544
+ assert_equal({ "__typename" => "UnauthorizedObject" }, visible_response["data"]["unauthorizedObject"])
545
+ end
546
+
547
+ it "halts on unauthorized mutations" do
548
+ query = "mutation { doUnauthorizedStuff(input: {}) { __typename } }"
549
+ res = auth_execute(query, context: { unauthorized_mutation: true })
550
+ assert_nil res["data"].fetch("doUnauthorizedStuff")
551
+ assert_raises NotImplementedError do
552
+ auth_execute(query)
553
+ end
554
+ end
555
+
556
+ it "halts on unauthorized fields, using the parent object" do
557
+ query = "{ unauthorized }"
558
+ hidden_response = auth_execute(query, root_value: :hide)
559
+ assert_nil hidden_response["data"].fetch("unauthorized")
560
+ visible_response = auth_execute(query, root_value: 1)
561
+ assert_equal 1, visible_response["data"]["unauthorized"]
562
+ end
563
+
564
+ it "halts on unauthorized arguments, using the parent object" do
565
+ query = "{ int2(unauthorized: 5) }"
566
+ hidden_response = auth_execute(query, root_value: :hide2)
567
+ assert_nil hidden_response["data"].fetch("int2")
568
+ visible_response = auth_execute(query)
569
+ assert_equal 5, visible_response["data"]["int2"]
570
+ end
571
+
572
+ it "works with edges and connections" do
573
+ skip <<-MSG
574
+ This doesn't work because edge and connection type definitions
575
+ aren't class-based, and authorization is checked during class-based field execution.
576
+ MSG
577
+ query = <<-GRAPHQL
578
+ {
579
+ unauthorizedConnection { __typename }
580
+ unauthorizedEdge { __typename }
581
+ }
582
+ GRAPHQL
583
+
584
+ unauthorized_res = auth_execute(query, context: { unauthorized_relay: true })
585
+ assert_nil unauthorized_res["data"].fetch("unauthorizedConnection")
586
+ assert_nil unauthorized_res["data"].fetch("unauthorizedEdge")
587
+
588
+ authorized_res = auth_execute(query)
589
+ assert_nil authorized_res["data"].fetch("unauthorizedConnection").fetch("__typename")
590
+ assert_nil authorized_res["data"].fetch("unauthorizedEdge").fetch("__typename")
591
+ end
592
+
593
+ it "authorizes _after_ resolving lazy objects" do
594
+ query = <<-GRAPHQL
595
+ {
596
+ a: unauthorizedLazyBox(value: "a") { value }
597
+ b: unauthorizedLazyBox(value: "b") { value }
598
+ }
599
+ GRAPHQL
600
+
601
+ unauthorized_res = auth_execute(query)
602
+ assert_nil unauthorized_res["data"].fetch("a")
603
+ assert_equal "b", unauthorized_res["data"]["b"]["value"]
604
+ end
605
+
606
+ it "authorizes items in a list" do
607
+ query = <<-GRAPHQL
608
+ {
609
+ unauthorizedListItems { __typename }
610
+ }
611
+ GRAPHQL
612
+
613
+ unauthorized_res = auth_execute(query, context: { hide: true })
614
+
615
+ assert_nil unauthorized_res["data"]["unauthorizedListItems"]
616
+ authorized_res = auth_execute(query, context: { hide: false })
617
+ assert_equal 2, authorized_res["data"]["unauthorizedListItems"].size
618
+ end
619
+
620
+ it "syncs lazy objects from authorized? checks" do
621
+ query = <<-GRAPHQL
622
+ {
623
+ a: unauthorizedLazyCheckBox(value: "a") { value }
624
+ b: unauthorizedLazyCheckBox(value: "b") { value }
625
+ }
626
+ GRAPHQL
627
+
628
+ unauthorized_res = auth_execute(query)
629
+ assert_nil unauthorized_res["data"].fetch("a")
630
+ assert_equal "b", unauthorized_res["data"]["b"]["value"]
631
+ # Also, the custom handler was called:
632
+ assert_equal ["Unauthorized UnauthorizedCheckBox: a"], unauthorized_res["errors"].map { |e| e["message"] }
633
+ end
634
+
635
+ it "Works for lazy connections" do
636
+ query = <<-GRAPHQL
637
+ {
638
+ lazyIntegers { edges { node { value } } }
639
+ }
640
+ GRAPHQL
641
+ res = auth_execute(query)
642
+ assert_equal [1,2,3], res["data"]["lazyIntegers"]["edges"].map { |e| e["node"]["value"] }
643
+ end
644
+
645
+ it "Works for eager connections" do
646
+ query = <<-GRAPHQL
647
+ {
648
+ integers { edges { node { value } } }
649
+ }
650
+ GRAPHQL
651
+ res = auth_execute(query)
652
+ assert_equal [1,2,3], res["data"]["integers"]["edges"].map { |e| e["node"]["value"] }
653
+ end
654
+
655
+ it "filters out individual nodes by value" do
656
+ query = <<-GRAPHQL
657
+ {
658
+ integers { edges { node { value } } }
659
+ }
660
+ GRAPHQL
661
+ res = auth_execute(query, context: { exclude_integer: 1 })
662
+ assert_equal [nil,2,3], res["data"]["integers"]["edges"].map { |e| e["node"] && e["node"]["value"] }
663
+ assert_equal ["Unauthorized IntegerObject: 1"], res["errors"].map { |e| e["message"] }
664
+ end
665
+
666
+ it "works with lazy values / interfaces" do
667
+ query = <<-GRAPHQL
668
+ query($value: String!){
669
+ unauthorizedInterface(value: $value) {
670
+ ... on UnauthorizedCheckBox {
671
+ value
672
+ }
673
+ }
674
+ }
675
+ GRAPHQL
676
+
677
+ res = auth_execute(query, variables: { value: "a"})
678
+ assert_nil res["data"]["unauthorizedInterface"]
679
+
680
+ res2 = auth_execute(query, variables: { value: "b"})
681
+ assert_equal "b", res2["data"]["unauthorizedInterface"]["value"]
682
+ end
683
+ end
684
+ end