graphql 1.8.3 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/graphql.rb +4 -1
- data/lib/graphql/argument.rb +1 -0
- data/lib/graphql/authorization.rb +81 -0
- data/lib/graphql/boolean_type.rb +0 -1
- data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +2 -1
- data/lib/graphql/execution/execute.rb +34 -10
- data/lib/graphql/execution/lazy.rb +5 -1
- data/lib/graphql/field.rb +7 -1
- data/lib/graphql/float_type.rb +0 -1
- data/lib/graphql/id_type.rb +0 -1
- data/lib/graphql/int_type.rb +0 -1
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/object_type.rb +3 -3
- data/lib/graphql/query.rb +6 -0
- data/lib/graphql/query/arguments.rb +2 -0
- data/lib/graphql/query/context.rb +6 -0
- data/lib/graphql/query/variables.rb +7 -1
- data/lib/graphql/relay/connection_instrumentation.rb +2 -2
- data/lib/graphql/relay/connection_resolve.rb +7 -27
- data/lib/graphql/relay/connection_type.rb +1 -0
- data/lib/graphql/relay/edge_type.rb +1 -0
- data/lib/graphql/relay/edges_instrumentation.rb +9 -25
- data/lib/graphql/relay/mutation/instrumentation.rb +1 -2
- data/lib/graphql/relay/mutation/resolve.rb +2 -4
- data/lib/graphql/relay/node.rb +1 -6
- data/lib/graphql/relay/page_info.rb +1 -9
- data/lib/graphql/schema.rb +84 -11
- data/lib/graphql/schema/argument.rb +13 -0
- data/lib/graphql/schema/enum.rb +1 -1
- data/lib/graphql/schema/enum_value.rb +4 -0
- data/lib/graphql/schema/field.rb +44 -11
- data/lib/graphql/schema/interface.rb +20 -0
- data/lib/graphql/schema/introspection_system.rb +1 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +25 -3
- data/lib/graphql/schema/member/instrumentation.rb +15 -17
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/object.rb +33 -0
- data/lib/graphql/schema/possible_types.rb +2 -0
- data/lib/graphql/schema/resolver.rb +10 -0
- data/lib/graphql/schema/traversal.rb +9 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +11 -2
- data/lib/graphql/string_type.rb +0 -1
- data/lib/graphql/types.rb +7 -0
- data/lib/graphql/types/relay.rb +31 -0
- data/lib/graphql/types/relay/base_connection.rb +87 -0
- data/lib/graphql/types/relay/base_edge.rb +51 -0
- data/lib/graphql/types/relay/base_field.rb +22 -0
- data/lib/graphql/types/relay/base_interface.rb +29 -0
- data/lib/graphql/types/relay/base_object.rb +26 -0
- data/lib/graphql/types/relay/node.rb +18 -0
- data/lib/graphql/types/relay/page_info.rb +23 -0
- data/lib/graphql/unauthorized_error.rb +20 -0
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/authorization_spec.rb +684 -0
- data/spec/graphql/query/variables_spec.rb +20 -0
- data/spec/graphql/relay/connection_instrumentation_spec.rb +1 -1
- data/spec/graphql/schema/resolver_spec.rb +31 -0
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +52 -0
- data/spec/support/dummy/schema.rb +16 -0
- data/spec/support/star_wars/schema.rb +28 -17
- 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
|
data/lib/graphql/version.rb
CHANGED
@@ -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
|