rails-graphql 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rails/graphql.rb +1 -3
- data/lib/rails/graphql/callback.rb +23 -13
- data/lib/rails/graphql/errors.rb +4 -0
- data/lib/rails/graphql/event.rb +16 -1
- data/lib/rails/graphql/field.rb +9 -4
- data/lib/rails/graphql/field/authorized_field.rb +33 -0
- data/lib/rails/graphql/field/mutation_field.rb +2 -1
- data/lib/rails/graphql/field/output_field.rb +4 -0
- data/lib/rails/graphql/field/proxied_field.rb +5 -0
- data/lib/rails/graphql/field/resolved_field.rb +4 -4
- data/lib/rails/graphql/field/scoped_config.rb +1 -1
- data/lib/rails/graphql/helpers/with_callbacks.rb +1 -1
- data/lib/rails/graphql/helpers/with_events.rb +8 -8
- data/lib/rails/graphql/helpers/with_owner.rb +10 -2
- data/lib/rails/graphql/helpers/with_schema_fields.rb +5 -1
- data/lib/rails/graphql/request.rb +7 -3
- data/lib/rails/graphql/request/component/field.rb +9 -4
- data/lib/rails/graphql/request/event.rb +4 -4
- data/lib/rails/graphql/request/helpers/directives.rb +3 -2
- data/lib/rails/graphql/request/steps/authorizable.rb +101 -0
- data/lib/rails/graphql/request/strategy.rb +1 -1
- data/lib/rails/graphql/source/active_record_source.rb +5 -5
- data/lib/rails/graphql/source/scoped_arguments.rb +1 -1
- data/lib/rails/graphql/type_map.rb +3 -3
- data/lib/rails/graphql/version.rb +1 -1
- data/test/integration/authorization/authorization_test.rb +112 -0
- data/test/integration/schemas/authorization.rb +12 -0
- data/test/test_ext.rb +14 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a4e4946f91fcba1ff47ce9ba25b56af87b2a5652f1d414b4d949b46d8e81bb5
|
4
|
+
data.tar.gz: dbd510340b18b079ace3237f5ffb9721b752af2795e34e8778c5687dc9d2af1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df0dbeb5e6d27955c328d7f1c7051324ca47f71e279df2fa0e927cf0d9af0b150c33c209fa1fd3fc09701ce5e0df0137e88c8faff04bf602bc798dd2eda4ea71
|
7
|
+
data.tar.gz: 03f61e059ebea710f08d22452dc736d35efae12174b3a74aff67490fee80b8edf08124f2b17f43656f525b893a548b2c99ff2607dc5442059fa2ab6fef71ca4e
|
data/lib/rails/graphql.rb
CHANGED
@@ -136,9 +136,7 @@ module Rails # :nodoc:
|
|
136
136
|
|
137
137
|
if (source = xargs.delete(:source)).present?
|
138
138
|
location = xargs.delete(:location) || source.try(:directive_location)
|
139
|
-
event ||= GraphQL::Event.new(:attach, source, **xargs
|
140
|
-
phase: :definition,
|
141
|
-
))
|
139
|
+
event ||= GraphQL::Event.new(:attach, source, phase: :definition, **xargs)
|
142
140
|
end
|
143
141
|
|
144
142
|
Array.wrap(list).each_with_object(Set.new) do |item, result|
|
@@ -15,7 +15,7 @@ module Rails # :nodoc:
|
|
15
15
|
|
16
16
|
# Directives need to be contextualized by the given instance as +context+
|
17
17
|
def self.set_context(item, context)
|
18
|
-
lambda { |*args| item.call(*args,
|
18
|
+
lambda { |*args, **xargs| item.call(*args, _callback_context: context, **xargs) }
|
19
19
|
end
|
20
20
|
|
21
21
|
def initialize(target, event_name, *args, **xargs, &block)
|
@@ -46,9 +46,12 @@ module Rails # :nodoc:
|
|
46
46
|
|
47
47
|
# This does the whole checking and preparation in order to really execute
|
48
48
|
# the callback method
|
49
|
-
def call(event,
|
49
|
+
def call(event, *args, _callback_context: nil, **xargs)
|
50
50
|
return unless event.name === event_name && can_run?(event)
|
51
|
-
|
51
|
+
|
52
|
+
block.is_a?(Symbol) \
|
53
|
+
? call_symbol(event, *args, **xargs) \
|
54
|
+
: call_proc(event, _callback_context, *args, **xargs)
|
52
55
|
end
|
53
56
|
|
54
57
|
# Get a described source location for the callback
|
@@ -68,38 +71,45 @@ module Rails # :nodoc:
|
|
68
71
|
|
69
72
|
private
|
70
73
|
|
74
|
+
# Find the proper owner of the symbol based callback
|
75
|
+
def owner
|
76
|
+
@owner ||= target.all_owners.find do |item|
|
77
|
+
item.is_a?(Class) ? item.method_defined?(block) : item.respond_to?(block)
|
78
|
+
end || target
|
79
|
+
end
|
80
|
+
|
71
81
|
# Using the filters, check if the current callback can be executed
|
72
82
|
def can_run?(event)
|
73
83
|
filters.all? { |key, options| event_filters[key][:block].call(options, event) }
|
74
84
|
end
|
75
85
|
|
76
86
|
# Call the callback block as a symbol
|
77
|
-
def call_symbol(event)
|
78
|
-
owner = target.try(:proxied_owner) || target.try(:owner) || target
|
87
|
+
def call_symbol(event, *args, **xargs)
|
79
88
|
event.on_instance(owner) do |instance|
|
80
89
|
block = instance.method(@block)
|
81
|
-
args, xargs = collect_parameters(event, block)
|
90
|
+
args, xargs = collect_parameters(event, [args, xargs], block)
|
82
91
|
block.call(*args, **xargs)
|
83
92
|
end
|
84
93
|
end
|
85
94
|
|
86
95
|
# Call the callback block as a proc
|
87
|
-
def call_proc(event, context = nil)
|
88
|
-
args, xargs = collect_parameters(event)
|
96
|
+
def call_proc(event, context = nil, *args, **xargs)
|
97
|
+
args, xargs = collect_parameters(event, [args, xargs])
|
89
98
|
(context || event).instance_exec(*args, **xargs, &block)
|
90
99
|
end
|
91
100
|
|
92
101
|
# Read the arguments needed for a block then collect them from the
|
93
102
|
# event and return the execution args
|
94
|
-
def collect_parameters(event, block = @block)
|
95
|
-
args_source = event.send(:args_source)
|
96
|
-
|
97
|
-
|
103
|
+
def collect_parameters(event, send_args, block = @block)
|
104
|
+
args_source = event.send(:args_source)
|
105
|
+
send_args[0] += @pre_args.deep_dup
|
106
|
+
send_args[1].merge!(@pre_xargs.deep_dup)
|
107
|
+
return send_args unless inject_arguments?
|
98
108
|
|
99
109
|
# TODO: Maybe we need to turn procs into lambdas so the optional
|
100
110
|
# arguments doesn't suffer any kind of change
|
101
111
|
idx = -1
|
102
|
-
block.parameters.each_with_object(
|
112
|
+
block.parameters.each_with_object(send_args) do |(type, name), result|
|
103
113
|
case type
|
104
114
|
when :opt, :req
|
105
115
|
idx += 1
|
data/lib/rails/graphql/errors.rb
CHANGED
@@ -38,5 +38,9 @@ module Rails # :nodoc:
|
|
38
38
|
# Error class related to when the captured output value is invalid due to
|
39
39
|
# type checking
|
40
40
|
InvalidValueError = Class.new(FieldError)
|
41
|
+
|
42
|
+
# Error class related to when a field is unauthorized and can not be used,
|
43
|
+
# similar to disabled fields
|
44
|
+
UnauthorizedFieldError = Class.new(FieldError)
|
41
45
|
end
|
42
46
|
end
|
data/lib/rails/graphql/event.rb
CHANGED
@@ -108,7 +108,7 @@ module Rails # :nodoc:
|
|
108
108
|
|
109
109
|
# Call a given block and send the event as reference
|
110
110
|
def trigger(block)
|
111
|
-
catchable(:item) {
|
111
|
+
@last_result = catchable(:item) { block.call(self) }
|
112
112
|
end
|
113
113
|
|
114
114
|
# Stop the execution of an event using a given +layer+. The default is to
|
@@ -127,6 +127,10 @@ module Rails # :nodoc:
|
|
127
127
|
|
128
128
|
alias call_super call_next
|
129
129
|
|
130
|
+
protected
|
131
|
+
|
132
|
+
alias args_source itself
|
133
|
+
|
130
134
|
private
|
131
135
|
|
132
136
|
# Add the layer, exec the block and remove the layer
|
@@ -136,6 +140,17 @@ module Rails # :nodoc:
|
|
136
140
|
ensure
|
137
141
|
@layers.pop
|
138
142
|
end
|
143
|
+
|
144
|
+
# Check for data based readers
|
145
|
+
def respond_to_missing?(method_name, include_private = false)
|
146
|
+
data.key?(method_name) || super
|
147
|
+
end
|
148
|
+
|
149
|
+
# If the +method_name+ matches any key entry of the provided data, just
|
150
|
+
# return the value stored there
|
151
|
+
def method_missing(method_name, *)
|
152
|
+
data.key?(method_name) ? data[method_name] : super
|
153
|
+
end
|
139
154
|
end
|
140
155
|
end
|
141
156
|
end
|
data/lib/rails/graphql/field.rb
CHANGED
@@ -38,20 +38,20 @@ module Rails # :nodoc:
|
|
38
38
|
|
39
39
|
autoload :ScopedConfig
|
40
40
|
|
41
|
+
autoload :AuthorizedField
|
42
|
+
autoload :ProxiedField
|
41
43
|
autoload :ResolvedField
|
42
44
|
autoload :TypedField
|
43
|
-
autoload :ProxiedField
|
44
45
|
|
45
46
|
autoload :InputField
|
46
47
|
autoload :OutputField
|
47
48
|
autoload :MutationField
|
48
49
|
|
49
|
-
|
50
|
+
attr_reader :name, :gql_name, :owner
|
50
51
|
|
52
|
+
delegate :input_type?, :output_type?, :leaf_type?, :proxy?, :mutation?, to: :class
|
51
53
|
delegate :namespaces, to: :owner
|
52
54
|
|
53
|
-
attr_reader :name, :gql_name, :owner
|
54
|
-
|
55
55
|
class << self
|
56
56
|
# A small shared helper method that allows field information to be
|
57
57
|
# proxied
|
@@ -145,6 +145,11 @@ module Rails # :nodoc:
|
|
145
145
|
(other.nullable? == nullable? || other.nullable? && !nullable?)
|
146
146
|
end
|
147
147
|
|
148
|
+
# Return the owner as the single item of the list
|
149
|
+
def all_owners
|
150
|
+
[owner]
|
151
|
+
end
|
152
|
+
|
148
153
|
# Checks if the argument can be null
|
149
154
|
def null?
|
150
155
|
!!@null
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails # :nodoc:
|
4
|
+
module GraphQL # :nodoc:
|
5
|
+
# This provides ways for fields to be authorized, giving a logical level for
|
6
|
+
# enabling or disabling access to a field. It has a similar structure to
|
7
|
+
# events, but has a different hierarchy of resolution
|
8
|
+
module Field::AuthorizedField
|
9
|
+
# Just add the callbacks setup to the field
|
10
|
+
def self.included(other)
|
11
|
+
other.event_types(:authorize, append: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add either settings for authorization or a block to be executed. It
|
15
|
+
# returns +self+ for chain purposes
|
16
|
+
def authorize(*args, **xargs, &block)
|
17
|
+
@authorizer = [args, xargs, block]
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return the settings for the authorize process
|
22
|
+
def authorizer
|
23
|
+
@authorizer if authorizable?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Checks if the field should go through an authorization process
|
27
|
+
def authorizable?
|
28
|
+
defined?(@authorizer)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
#
|
@@ -16,9 +16,10 @@ module Rails # :nodoc:
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# Add a block or a callable method that is executed before the resolver
|
19
|
-
# but after all the before resolve
|
19
|
+
# but after all the before resolve. It returns +self+ for chain purposes
|
20
20
|
def perform(*args, **xargs, &block)
|
21
21
|
@performer = Callback.new(self, :perform, *args, **xargs, &block)
|
22
|
+
self
|
22
23
|
end
|
23
24
|
|
24
25
|
# Get the performer that can be already defined or used through the
|
@@ -10,6 +10,10 @@ module Rails # :nodoc:
|
|
10
10
|
class Field::OutputField < Field
|
11
11
|
include Helpers::WithArguments
|
12
12
|
include Helpers::WithValidator
|
13
|
+
include Helpers::WithEvents
|
14
|
+
include Helpers::WithCallbacks
|
15
|
+
|
16
|
+
include Field::AuthorizedField
|
13
17
|
include Field::ResolvedField
|
14
18
|
include Field::TypedField
|
15
19
|
|
@@ -29,16 +29,16 @@ module Rails # :nodoc:
|
|
29
29
|
|
30
30
|
# Just add the callbacks setup to the field
|
31
31
|
def self.included(other)
|
32
|
-
other.
|
33
|
-
other.include(Helpers::WithCallbacks)
|
34
|
-
other.event_types(:prepare, :finalize, expose: true)
|
32
|
+
other.event_types(:prepare, :finalize, append: true, expose: true)
|
35
33
|
other.alias_method(:before_resolve, :prepare)
|
36
34
|
other.alias_method(:after_resolve, :finalize)
|
37
35
|
end
|
38
36
|
|
39
|
-
# Add a block that is performed while resolving a value of a field
|
37
|
+
# Add a block that is performed while resolving a value of a field. It
|
38
|
+
# returns +self+ for chain purposes
|
40
39
|
def resolve(*args, **xargs, &block)
|
41
40
|
@resolver = Callback.new(self, :resolve, *args, **xargs, &block)
|
41
|
+
self
|
42
42
|
end
|
43
43
|
|
44
44
|
# Get the resolver that can be already defined or used through the
|
@@ -6,7 +6,7 @@ module Rails # :nodoc:
|
|
6
6
|
# instance of this class works as proxy for changes to the actual field.
|
7
7
|
Field::ScopedConfig = Struct.new(:field, :receiver) do
|
8
8
|
delegate :argument, :ref_argument, :id_argument, :use, :internal?, :disabled?,
|
9
|
-
:enabled?, :disable!, :enable!, to: :field
|
9
|
+
:enabled?, :disable!, :enable!, :authorize, to: :field
|
10
10
|
|
11
11
|
delegate_missing_to :receiver
|
12
12
|
|
@@ -9,7 +9,7 @@ module Rails # :nodoc:
|
|
9
9
|
# symbolic methods
|
10
10
|
module WithCallbacks
|
11
11
|
DEFAULT_EVENT_TYPES = %i[query mutation subscription request attach
|
12
|
-
organized prepared finalize].freeze
|
12
|
+
authorize organized prepared finalize].freeze
|
13
13
|
|
14
14
|
def self.extended(other)
|
15
15
|
other.extend(WithCallbacks::Setup)
|
@@ -27,20 +27,20 @@ module Rails # :nodoc:
|
|
27
27
|
# Set or get the list of possible event types when attaching events
|
28
28
|
def event_types(*list, append: false, expose: false)
|
29
29
|
return (defined?(@event_types) && @event_types.presence) ||
|
30
|
-
superclass.try(:event_types) if list.blank?
|
30
|
+
superclass.try(:event_types) || [] if list.blank?
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
@event_types =
|
35
|
-
expose_events! if expose
|
32
|
+
new_list = append ? event_types : []
|
33
|
+
new_list += list.flatten.compact.map(&:to_sym)
|
34
|
+
@event_types = new_list.uniq.freeze
|
35
|
+
expose_events!(*list) if expose
|
36
36
|
@event_types
|
37
37
|
end
|
38
38
|
|
39
39
|
protected
|
40
40
|
|
41
41
|
# Auxiliar method that creates easy-accessible callback assignment
|
42
|
-
def expose_events!
|
43
|
-
|
42
|
+
def expose_events!(*list)
|
43
|
+
list.each do |event_name|
|
44
44
|
next if method_defined?(event_name)
|
45
45
|
define_method(event_name) do |*args, **xargs, &block|
|
46
46
|
on(event_name, *args, **xargs, &block)
|
@@ -51,7 +51,7 @@ module Rails # :nodoc:
|
|
51
51
|
|
52
52
|
# Mostly for correct inheritance on instances
|
53
53
|
def all_events
|
54
|
-
current = (@events
|
54
|
+
current = defined?(@events) ? @events : {}
|
55
55
|
return current unless defined? super
|
56
56
|
Helpers.merge_hash_array(current, super)
|
57
57
|
end
|
@@ -6,9 +6,16 @@ module Rails # :nodoc:
|
|
6
6
|
# Helper module that allows other objects to hold an +assigned_to+ object
|
7
7
|
module WithOwner
|
8
8
|
def self.included(other)
|
9
|
+
other.extend(WithOwner::ClassMethods)
|
9
10
|
other.class_attribute(:owner, instance_writer: false)
|
10
11
|
end
|
11
12
|
|
13
|
+
module ClassMethods # :nodoc: all
|
14
|
+
def method_defined?(method_name)
|
15
|
+
super || owner&.method_defined?(method_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
12
19
|
private
|
13
20
|
|
14
21
|
def respond_to_missing?(*args) # :nodoc:
|
@@ -24,8 +31,9 @@ module Rails # :nodoc:
|
|
24
31
|
|
25
32
|
# Since owners are classes, this checks for the instance methods of
|
26
33
|
# it, since this is a instance method
|
27
|
-
def owner_respond_to?(method_name,
|
28
|
-
|
34
|
+
def owner_respond_to?(method_name, with_private = false)
|
35
|
+
return true if !owner.is_a?(Class) && owner.respond_to?(method_name, with_private)
|
36
|
+
(with_private ? %i[public protected private] : %i[public]).any? do |type|
|
29
37
|
owner.send("#{type}_instance_methods").include?(method_name)
|
30
38
|
end unless owner.nil?
|
31
39
|
end
|
@@ -189,7 +189,11 @@ module Rails # :nodoc:
|
|
189
189
|
SCHEMA_FIELD_TYPES.each do |kind, type_name|
|
190
190
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
191
191
|
def #{kind}_field?(name)
|
192
|
-
|
192
|
+
has_field?(:#{kind}, name)
|
193
|
+
end
|
194
|
+
|
195
|
+
def #{kind}_field(name)
|
196
|
+
find_field(:#{kind}, name)
|
193
197
|
end
|
194
198
|
|
195
199
|
def #{kind}_fields(&block)
|
@@ -20,6 +20,7 @@ module Rails # :nodoc:
|
|
20
20
|
|
21
21
|
eager_autoload do
|
22
22
|
autoload_under :steps do
|
23
|
+
autoload :Authorizable
|
23
24
|
autoload :Organizable
|
24
25
|
autoload :Prepareable
|
25
26
|
autoload :Resolveable
|
@@ -42,8 +43,6 @@ module Rails # :nodoc:
|
|
42
43
|
attr_reader :schema, :visitor, :operations, :fragments, :errors,
|
43
44
|
:args, :response, :strategy, :stack
|
44
45
|
|
45
|
-
delegate :all_listeners, to: :schema
|
46
|
-
|
47
46
|
class << self
|
48
47
|
# Shortcut for initialize, set context, and execute
|
49
48
|
def execute(*args, schema: nil, namespace: :base, context: {}, **xargs)
|
@@ -79,6 +78,11 @@ module Rails # :nodoc:
|
|
79
78
|
ensure_schema!
|
80
79
|
end
|
81
80
|
|
81
|
+
# Cache all the schema listeners for this current request
|
82
|
+
def all_listeners
|
83
|
+
@all_listeners ||= schema.all_listeners
|
84
|
+
end
|
85
|
+
|
82
86
|
# Cache all the schema events for this current request
|
83
87
|
def all_events
|
84
88
|
@all_events ||= schema.all_events
|
@@ -127,7 +131,7 @@ module Rails # :nodoc:
|
|
127
131
|
# Add the given +exception+ to the errors using the +node+ location
|
128
132
|
def exception_to_error(exception, node, **xargs)
|
129
133
|
xargs[:exception] = exception.class.name
|
130
|
-
report_node_error(exception.message, node, **xargs)
|
134
|
+
report_node_error(xargs.delete(:message) || exception.message, node, **xargs)
|
131
135
|
end
|
132
136
|
|
133
137
|
# A little helper to report an error on a given node
|
@@ -8,6 +8,7 @@ module Rails # :nodoc:
|
|
8
8
|
# This class holds information about a given field that should be
|
9
9
|
# collected from the source of where it was requested.
|
10
10
|
class Component::Field < Component
|
11
|
+
include Authorizable
|
11
12
|
include ValueWriters
|
12
13
|
include SelectionSet
|
13
14
|
include Directives
|
@@ -40,13 +41,16 @@ module Rails # :nodoc:
|
|
40
41
|
# Override that considers the requested field directives and also the
|
41
42
|
# definition field events, both from itself and its directives events
|
42
43
|
def all_listeners
|
43
|
-
field.all_listeners + super
|
44
|
+
(request.cache(:listeners)[field] ||= field.all_listeners) + super
|
44
45
|
end
|
45
46
|
|
46
47
|
# Override that considers the requested field directives and also the
|
47
48
|
# definition field events, both from itself and its directives events
|
48
49
|
def all_events
|
49
|
-
@all_events ||= Helpers.merge_hash_array(
|
50
|
+
@all_events ||= Helpers.merge_hash_array(
|
51
|
+
(request.cache(:events)[field] ||= field.all_events),
|
52
|
+
super,
|
53
|
+
)
|
50
54
|
end
|
51
55
|
|
52
56
|
# Get and cache all the arguments for the field
|
@@ -129,6 +133,7 @@ module Rails # :nodoc:
|
|
129
133
|
def organize_then(&block)
|
130
134
|
super(block) do
|
131
135
|
check_assignment!
|
136
|
+
check_authorization!
|
132
137
|
|
133
138
|
parse_arguments
|
134
139
|
parse_directives
|
@@ -183,10 +188,10 @@ module Rails # :nodoc:
|
|
183
188
|
def trigger_event(event_name, **xargs)
|
184
189
|
return super if !defined?(@current_object) || @current_object.nil?
|
185
190
|
|
186
|
-
listeners = request.cache(:
|
191
|
+
listeners = request.cache(:listeners)[field] ||= field.all_listeners
|
187
192
|
return super unless listeners.include?(event_name)
|
188
193
|
|
189
|
-
callbacks = request.cache(:
|
194
|
+
callbacks = request.cache(:events)[field] ||= field.all_events
|
190
195
|
old_events, @all_events = @all_events, callbacks
|
191
196
|
super
|
192
197
|
ensure
|
@@ -70,13 +70,13 @@ module Rails # :nodoc:
|
|
70
70
|
args_source.try(:[], name.to_sym)
|
71
71
|
end
|
72
72
|
|
73
|
+
alias arg argument
|
74
|
+
|
73
75
|
# A combined helper for +instance_for+ and +set_on+
|
74
|
-
def on_instance(
|
75
|
-
set_on(
|
76
|
+
def on_instance(object, &block)
|
77
|
+
set_on(object.is_a?(Class) ? instance_for(object) : object, &block)
|
76
78
|
end
|
77
79
|
|
78
|
-
alias arg argument
|
79
|
-
|
80
80
|
protected
|
81
81
|
|
82
82
|
# When performing an event under a field object, the keyed-based
|
@@ -6,12 +6,13 @@ module Rails # :nodoc:
|
|
6
6
|
# Helper module to collect the directives from fragments, operations, and
|
7
7
|
# fields.
|
8
8
|
module Directives
|
9
|
-
# Get the list of listeners from
|
9
|
+
# Get the list of listeners from directives set during the request only
|
10
10
|
def all_listeners
|
11
11
|
directives.map(&:all_listeners).reduce(:+) || Set.new
|
12
12
|
end
|
13
13
|
|
14
|
-
# Get the list of events from
|
14
|
+
# Get the list of events from directives set during the request only and
|
15
|
+
# then caches it by request
|
15
16
|
def all_events
|
16
17
|
@all_events ||= directives.map(&:all_events).inject({}) do |lhash, rhash|
|
17
18
|
Helpers.merge_hash_array(lhash, rhash)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails # :nodoc:
|
4
|
+
module GraphQL # :nodoc:
|
5
|
+
class Request # :nodoc:
|
6
|
+
# Helper methods for the authorize step of a request
|
7
|
+
module Authorizable
|
8
|
+
# Event used to perform an authorization step
|
9
|
+
class Event < GraphQL::Event
|
10
|
+
# Similar to trigger for object, but with an extra extension for
|
11
|
+
# instance methods defined on the given object
|
12
|
+
def authorize_using(object, send_args, events = nil)
|
13
|
+
cache = data[:request].cache(name)[object] ||= []
|
14
|
+
return false if cache.present? && cache.none?
|
15
|
+
args, xargs = send_args
|
16
|
+
|
17
|
+
# Authorize through instance method
|
18
|
+
using_object = cache[0] ||= authorize_on_object(object)
|
19
|
+
set_on(using_object) do |instance|
|
20
|
+
instance.public_send("#{name}!", self, *args, **xargs)
|
21
|
+
end if using_object
|
22
|
+
|
23
|
+
# Authorize through events
|
24
|
+
using_events = cache[1] ||= (events || object.all_events[name]).presence
|
25
|
+
using_events&.each { |block| block.call(self, *args, **xargs) }
|
26
|
+
|
27
|
+
# Does any authorize process ran
|
28
|
+
cache.any?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Simply unauthorize the operation
|
32
|
+
def unauthorize!(*, message: nil, **)
|
33
|
+
raise UnauthorizedFieldError, message || <<~MSG.squish
|
34
|
+
Unauthorized access to "#{field.gql_name}" field.
|
35
|
+
MSG
|
36
|
+
end
|
37
|
+
|
38
|
+
# Simply authorize the operation
|
39
|
+
def authorize!(*)
|
40
|
+
throw :authorized
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Check if it should run call an +authorize!+ method on the given
|
46
|
+
# +object+. Classes are turn into instance through strategy
|
47
|
+
def authorize_on_object(object)
|
48
|
+
as_class = object.is_a?(Class)
|
49
|
+
checker = as_class ? :method_defined? : :respond_to?
|
50
|
+
|
51
|
+
return false unless object.public_send(checker, :authorize!)
|
52
|
+
as_class ? data[:request].strategy.instance_for(object) : object
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check if the field is correctly authorized to be executed
|
57
|
+
def check_authorization!
|
58
|
+
return unless field.authorizable?
|
59
|
+
*args, block = field.authorizer
|
60
|
+
|
61
|
+
catch(:authorized) do
|
62
|
+
event = authorization_event
|
63
|
+
schema_events = request.all_events[:authorize]
|
64
|
+
executed = event.authorize_using(schema, args, schema_events)
|
65
|
+
|
66
|
+
element = field
|
67
|
+
while element && element != schema
|
68
|
+
executed = event.authorize_using(element, args) || executed
|
69
|
+
element = element.try(:owner)
|
70
|
+
end
|
71
|
+
|
72
|
+
if block.present?
|
73
|
+
block.call(event, *args[0], **args[1])
|
74
|
+
executed = true
|
75
|
+
end
|
76
|
+
|
77
|
+
event.unauthorize!(message: <<~MSG.squish) unless executed
|
78
|
+
Authorization required but unable to be executed
|
79
|
+
MSG
|
80
|
+
end
|
81
|
+
rescue UnauthorizedFieldError => error
|
82
|
+
request.rescue_with_handler(error)
|
83
|
+
request.exception_to_error(error, @node)
|
84
|
+
invalidate!
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Build and store the authorization event
|
90
|
+
def authorization_event
|
91
|
+
Event.new(:authorize, self,
|
92
|
+
request: request,
|
93
|
+
schema: schema,
|
94
|
+
field: field,
|
95
|
+
memo: memo,
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -37,6 +37,7 @@ module Rails # :nodoc:
|
|
37
37
|
|
38
38
|
def initialize(request)
|
39
39
|
@request = request
|
40
|
+
@objects_pool = {}
|
40
41
|
collect_request_listeners
|
41
42
|
end
|
42
43
|
|
@@ -205,7 +206,6 @@ module Rails # :nodoc:
|
|
205
206
|
# it can load data in a pretty smart way
|
206
207
|
def collect_data
|
207
208
|
@data_pool = {}
|
208
|
-
@objects_pool = {}
|
209
209
|
@context = request.build(Request::Context)
|
210
210
|
|
211
211
|
# TODO: Create an orchestrator to allow cross query loading
|
@@ -150,13 +150,13 @@ module Rails # :nodoc:
|
|
150
150
|
end
|
151
151
|
|
152
152
|
# Prepare to load multiple records from the underlying table
|
153
|
-
def load_records
|
154
|
-
inject_scopes(
|
153
|
+
def load_records(scope = model.default_scoped)
|
154
|
+
inject_scopes(scope, :relation)
|
155
155
|
end
|
156
156
|
|
157
157
|
# Prepare to load a single record from the underlying table
|
158
|
-
def load_record
|
159
|
-
|
158
|
+
def load_record(scope = model.default_scoped)
|
159
|
+
scope.find(event.argument(primary_key))
|
160
160
|
end
|
161
161
|
|
162
162
|
# Get the chain result and preload the records with thre resulting scope
|
@@ -166,7 +166,7 @@ module Rails # :nodoc:
|
|
166
166
|
|
167
167
|
# Collect a scope for filters applied to a given association
|
168
168
|
def build_association_scope(association)
|
169
|
-
scope = model._reflect_on_association(association).klass.
|
169
|
+
scope = model._reflect_on_association(association).klass.default_scoped
|
170
170
|
|
171
171
|
# Apply proxied injected scopes
|
172
172
|
proxied = event.field.try(:proxied_owner)
|
@@ -45,7 +45,7 @@ module Rails # :nodoc:
|
|
45
45
|
# Add a new scoped param to the list
|
46
46
|
def scoped_argument(param, type = :string, proc_method = nil, **settings, &block)
|
47
47
|
block = proc_method if proc_method.present? && block.nil?
|
48
|
-
argument = Argument.new(param, type, **settings
|
48
|
+
argument = Argument.new(param, type, **settings, owner: self, &block)
|
49
49
|
(@scoped_arguments ||= {})[argument.name] = argument
|
50
50
|
end
|
51
51
|
|
@@ -223,11 +223,11 @@ module Rails # :nodoc:
|
|
223
223
|
namespaces += [:base] unless namespaces.include?(:base) || exclusive
|
224
224
|
|
225
225
|
iterated = []
|
226
|
-
enumerator = Enumerator::Lazy.new(namespaces.uniq) do |yielder,
|
227
|
-
next unless @index.key?(
|
226
|
+
enumerator = Enumerator::Lazy.new(namespaces.uniq) do |yielder, item|
|
227
|
+
next unless @index.key?(item)
|
228
228
|
|
229
229
|
# Only iterate over string based types
|
230
|
-
@index[
|
230
|
+
@index[item][base_class]&.each do |_key, value|
|
231
231
|
next if iterated.include?(value = value.call) || value.blank?
|
232
232
|
iterated << value
|
233
233
|
yielder << value
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'integration/config'
|
2
|
+
|
3
|
+
class Integration_Memory_AuthorizationTest < GraphQL::IntegrationTestCase
|
4
|
+
load_schema 'authorization'
|
5
|
+
|
6
|
+
SCHEMA = ::AuthorizationSchema
|
7
|
+
EXCEPTION_NAME = 'Rails::GraphQL::UnauthorizedFieldError'
|
8
|
+
EXCEPTION_PATH = ['errors', 0, 'extensions', 'exception']
|
9
|
+
|
10
|
+
SAMPLE1 = { data: { sample1: 'Ok 1' } }
|
11
|
+
SAMPLE2 = { data: { sample2: 'Ok 2' } }
|
12
|
+
|
13
|
+
def test_field_event_types
|
14
|
+
assert_includes(SCHEMA.query_field(:sample1).event_types, :authorize)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_without_authorization
|
18
|
+
assert_result(SAMPLE1, '{ sample1 }')
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_with_simple_authorization
|
22
|
+
assert_exception('{ sample2 }')
|
23
|
+
assert_result({ sample1: 'Ok 1', sample2: nil }, '{ sample1 sample2 }', dig: 'data')
|
24
|
+
|
25
|
+
SCHEMA.stub_imethod(:authorize!, &:authorize!).call do
|
26
|
+
assert_result(SAMPLE2, '{ sample2 }')
|
27
|
+
end
|
28
|
+
|
29
|
+
SCHEMA.stub_imethod(:authorize!, &:unauthorize!).call do
|
30
|
+
assert_exception('{ sample2 }')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_with_block_authorization
|
35
|
+
field = SCHEMA.query_field(:sample1)
|
36
|
+
|
37
|
+
field.stub_ivar(:@authorizer, [[], {}, method(:executed!)]) do
|
38
|
+
assert_executed { assert_result(SAMPLE1, '{ sample1 }') }
|
39
|
+
end
|
40
|
+
|
41
|
+
block = ->(ev) { executed! && ev.unauthorize! }
|
42
|
+
field.stub_ivar(:@authorizer, [[], {}, block]) do
|
43
|
+
assert_executed { assert_exception('{ sample1 }') }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_with_field_event_authorization
|
48
|
+
field = SCHEMA.query_field(:sample2)
|
49
|
+
|
50
|
+
field.stub_ivar(:@events, { authorize: [method(:executed!)] }) do
|
51
|
+
assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
|
52
|
+
|
53
|
+
assert_executed do
|
54
|
+
field.on(:authorize) { |event| event.unauthorize! }
|
55
|
+
assert_exception('{ sample2 }')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_with_directive
|
61
|
+
field = SCHEMA.query_field(:sample2)
|
62
|
+
|
63
|
+
auth_directive = unmapped_class(Rails::GraphQL::Directive)
|
64
|
+
auth_directive.on(:authorize, &method(:executed!))
|
65
|
+
|
66
|
+
auth_directive.stub_ivar(:@gql_name, 'AuthDirective') do
|
67
|
+
field.stub_ivar(:@directives, [auth_directive.new]) do
|
68
|
+
assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
|
69
|
+
end
|
70
|
+
|
71
|
+
SCHEMA.stub_ivar(:@directives, [auth_directive.new]) do
|
72
|
+
assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_authorize_bypass
|
78
|
+
field = SCHEMA.query_field(:sample2)
|
79
|
+
auth_block = ->(e, event) { e.call && event.authorize! }.curry(2)[method(:executed!)]
|
80
|
+
unauth_block = ->(e, event) { e.call && event.unauthorize! }.curry(2)[method(:executed!)]
|
81
|
+
|
82
|
+
field.stub_ivar(:@events, { authorize: [unauth_block] }) do
|
83
|
+
SCHEMA.stub_imethod(:authorize!, &auth_block).call do
|
84
|
+
assert_executed { assert_result(SAMPLE2, '{ sample2 }') }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
field.stub_ivar(:@events, { authorize: [auth_block] }) do
|
89
|
+
SCHEMA.stub_imethod(:authorize!, &unauth_block).call do
|
90
|
+
assert_executed { assert_exception('{ sample2 }') }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
def executed!(*)
|
98
|
+
@executed += 1
|
99
|
+
end
|
100
|
+
|
101
|
+
def assert_executed(times = 1)
|
102
|
+
@executed = 0
|
103
|
+
yield
|
104
|
+
assert_equal(times, @executed)
|
105
|
+
ensure
|
106
|
+
remove_instance_variable(:@executed)
|
107
|
+
end
|
108
|
+
|
109
|
+
def assert_exception(query, *args, **xargs)
|
110
|
+
assert_result(EXCEPTION_NAME, query, *args, dig: EXCEPTION_PATH, **xargs)
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class AuthorizationSchema < GraphQL::Schema
|
2
|
+
namespace :authorization
|
3
|
+
|
4
|
+
configure do |config|
|
5
|
+
config.enable_string_collector = false
|
6
|
+
end
|
7
|
+
|
8
|
+
query_fields do
|
9
|
+
field(:sample1, :string).resolve { 'Ok 1' }
|
10
|
+
field(:sample2, :string).authorize.resolve { 'Ok 2' }
|
11
|
+
end
|
12
|
+
end
|
data/test/test_ext.rb
CHANGED
@@ -28,6 +28,20 @@ class Object < BasicObject
|
|
28
28
|
const_set(name, old_value) if defined? old_value
|
29
29
|
end
|
30
30
|
|
31
|
+
def stub_imethod(name, &block)
|
32
|
+
lambda do |&run_block|
|
33
|
+
alias_method(:"_old_#{name}", name) if (reset_old = method_defined?(name))
|
34
|
+
define_method(name, &block)
|
35
|
+
run_block.call
|
36
|
+
ensure
|
37
|
+
undef_method(name)
|
38
|
+
if reset_old
|
39
|
+
alias_method(name, :"_old_#{name}")
|
40
|
+
undef_method(:"_old_#{name}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
31
45
|
def get_reset_ivar(name, *extra, &block)
|
32
46
|
instance_variable_set(name, extra.first) if extra.any?
|
33
47
|
instance_exec(&block)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carlos Silva
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -307,6 +307,7 @@ files:
|
|
307
307
|
- lib/rails/graphql/errors.rb
|
308
308
|
- lib/rails/graphql/event.rb
|
309
309
|
- lib/rails/graphql/field.rb
|
310
|
+
- lib/rails/graphql/field/authorized_field.rb
|
310
311
|
- lib/rails/graphql/field/input_field.rb
|
311
312
|
- lib/rails/graphql/field/mutation_field.rb
|
312
313
|
- lib/rails/graphql/field/output_field.rb
|
@@ -355,6 +356,7 @@ files:
|
|
355
356
|
- lib/rails/graphql/request/helpers/directives.rb
|
356
357
|
- lib/rails/graphql/request/helpers/selection_set.rb
|
357
358
|
- lib/rails/graphql/request/helpers/value_writers.rb
|
359
|
+
- lib/rails/graphql/request/steps/authorizable.rb
|
358
360
|
- lib/rails/graphql/request/steps/organizable.rb
|
359
361
|
- lib/rails/graphql/request/steps/prepareable.rb
|
360
362
|
- lib/rails/graphql/request/steps/resolveable.rb
|
@@ -427,10 +429,12 @@ files:
|
|
427
429
|
- test/graphql/type_map_test.rb
|
428
430
|
- test/graphql/type_test.rb
|
429
431
|
- test/graphql_test.rb
|
432
|
+
- test/integration/authorization/authorization_test.rb
|
430
433
|
- test/integration/config.rb
|
431
434
|
- test/integration/memory/star_wars_introspection_test.rb
|
432
435
|
- test/integration/memory/star_wars_query_test.rb
|
433
436
|
- test/integration/memory/star_wars_validation_test.rb
|
437
|
+
- test/integration/schemas/authorization.rb
|
434
438
|
- test/integration/schemas/memory.rb
|
435
439
|
- test/integration/schemas/sqlite.rb
|
436
440
|
- test/integration/sqlite/star_wars_introspection_test.rb
|
@@ -503,7 +507,9 @@ test_files:
|
|
503
507
|
- test/integration/memory/star_wars_validation_test.rb
|
504
508
|
- test/integration/schemas/memory.rb
|
505
509
|
- test/integration/schemas/sqlite.rb
|
510
|
+
- test/integration/schemas/authorization.rb
|
506
511
|
- test/integration/sqlite/star_wars_introspection_test.rb
|
507
512
|
- test/integration/sqlite/star_wars_mutation_test.rb
|
508
513
|
- test/integration/sqlite/star_wars_query_test.rb
|
514
|
+
- test/integration/authorization/authorization_test.rb
|
509
515
|
- test/test_ext.rb
|