rails-graphql 0.1.0 → 0.2.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rails/graphql.rb +1 -3
  3. data/lib/rails/graphql/callback.rb +23 -13
  4. data/lib/rails/graphql/errors.rb +4 -0
  5. data/lib/rails/graphql/event.rb +16 -1
  6. data/lib/rails/graphql/field.rb +9 -4
  7. data/lib/rails/graphql/field/authorized_field.rb +33 -0
  8. data/lib/rails/graphql/field/input_field.rb +11 -1
  9. data/lib/rails/graphql/field/mutation_field.rb +4 -3
  10. data/lib/rails/graphql/field/output_field.rb +4 -0
  11. data/lib/rails/graphql/field/proxied_field.rb +5 -0
  12. data/lib/rails/graphql/field/resolved_field.rb +4 -4
  13. data/lib/rails/graphql/field/scoped_config.rb +1 -1
  14. data/lib/rails/graphql/helpers/with_assignment.rb +4 -2
  15. data/lib/rails/graphql/helpers/with_callbacks.rb +1 -1
  16. data/lib/rails/graphql/helpers/with_events.rb +8 -8
  17. data/lib/rails/graphql/helpers/with_owner.rb +10 -2
  18. data/lib/rails/graphql/helpers/with_schema_fields.rb +5 -1
  19. data/lib/rails/graphql/railties/controller.rb +1 -1
  20. data/lib/rails/graphql/request.rb +7 -3
  21. data/lib/rails/graphql/request/component/field.rb +9 -4
  22. data/lib/rails/graphql/request/errors.rb +2 -2
  23. data/lib/rails/graphql/request/event.rb +4 -4
  24. data/lib/rails/graphql/request/helpers/directives.rb +3 -2
  25. data/lib/rails/graphql/request/steps/authorizable.rb +101 -0
  26. data/lib/rails/graphql/request/strategy.rb +1 -1
  27. data/lib/rails/graphql/source.rb +5 -0
  28. data/lib/rails/graphql/source/active_record/builders.rb +1 -1
  29. data/lib/rails/graphql/source/active_record_source.rb +11 -6
  30. data/lib/rails/graphql/source/scoped_arguments.rb +1 -1
  31. data/lib/rails/graphql/type_map.rb +4 -4
  32. data/lib/rails/graphql/version.rb +1 -1
  33. data/test/integration/authorization/authorization_test.rb +112 -0
  34. data/test/integration/schemas/authorization.rb +12 -0
  35. data/test/test_ext.rb +14 -0
  36. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49e43687d5292b789a8bd31a328c7788e65ea18fa9e7210804a6d2b5fa626a55
4
- data.tar.gz: e18c7462267165fbc6c891cb6abcd8e1d58a7c4e4cef52209d8b7cd779775768
3
+ metadata.gz: f478dece4e018e58e675abb2f4fbff2edb9ad6279db0efae74037f4ed6c7479a
4
+ data.tar.gz: dc9d08df7234d4d5a833259f3feb95bfc088ed18201b43ebe70c2c62c76b5b40
5
5
  SHA512:
6
- metadata.gz: 9a389ad749b2de6f1109b81e675ab78adad048b2d621e268c2d22a7ed7ace9f76aa2ab7b82510a2089cdced461f3df721a89f3a86a9c4fca5215458266b274c3
7
- data.tar.gz: ac5b71676b2bded14527eed7d2b2b7242c3258f9ee0aaddc562d9ea7a58931aa1372d54634176058e0e1f4b959c712db95831dbcd2b1f15473652b13aa8126aa
6
+ metadata.gz: f532b284a2e9af983f8406d570b3b0dabaf017d588de1c211709536a7fbca3450d5ca0fc9850ffb5fd191e022d989b900111be1a347db41abfb40b5958f05cb1
7
+ data.tar.gz: 2465ad648d17fc618545fef5931c12add940805c35733d2424589c50dc14d2e9c4ae4620613ba8fc3f3e46eaaa34d1524fd9b18a2bb4ff05a0fc7b02139dd0d3
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.reverse_merge(
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, context: context) }
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, context: nil)
49
+ def call(event, *args, _callback_context: nil, **xargs)
50
50
  return unless event.name === event_name && can_run?(event)
51
- block.is_a?(Symbol) ? call_symbol(event) : call_proc(event, context)
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) || event
96
- start_args = [@pre_args.deep_dup, @pre_xargs.deep_dup]
97
- return start_args unless inject_arguments?
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(start_args) do |(type, name), result|
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
@@ -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
@@ -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) { @last_result = block.call(self) }
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
@@ -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
- delegate :input_type?, :output_type?, :leaf_type?, :proxy?, :mutation?, to: :class
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
+ #
@@ -55,7 +55,17 @@ module Rails # :nodoc:
55
55
 
56
56
  # Return the default value if the given +value+ is nil
57
57
  def deserialize(value = nil)
58
- value.nil? ? @default : super
58
+ value.nil? ? default : super
59
+ end
60
+
61
+ # A little override to use the default value
62
+ def to_json(value = nil)
63
+ super(value.nil? ? default : value)
64
+ end
65
+
66
+ # A little override to use the default value
67
+ def as_json(value = nil)
68
+ super(value.nil? ? default : value)
59
69
  end
60
70
 
61
71
  # Checks if the default value of the field is valid
@@ -16,16 +16,17 @@ 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
25
26
  # +method_name+ if that is callable
26
27
  def performer
27
- @performer ||= callable?(method_name) \
28
- ? Callback.new(self, :perform, method_name) \
28
+ @performer ||= callable?(:"#{method_name}!") \
29
+ ? Callback.new(self, :perform, :"#{method_name}!") \
29
30
  : false
30
31
  end
31
32
 
@@ -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
 
@@ -68,6 +68,11 @@ module Rails # :nodoc:
68
68
  field.owner
69
69
  end
70
70
 
71
+ # Override this to include proxied owners
72
+ def all_owners
73
+ super + proxied_owner.all_owners
74
+ end
75
+
71
76
  # Return the proxied field
72
77
  def proxied_field
73
78
  @field
@@ -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.include(Helpers::WithEvents)
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
 
@@ -54,14 +54,16 @@ module Rails # :nodoc:
54
54
  # Ignores the possible errors here related
55
55
  end
56
56
 
57
- # After a successfully registration, add the assigned class to the
58
- # type map as a great alias to find the object
57
+ # After a successfully registration, add the assigned class to the type
58
+ # map as a great alias to find the object, but only if the class does
59
+ # not have an alias already
59
60
  def register!
60
61
  return if abstract?
61
62
  return super unless assigned?
62
63
 
63
64
  result = super
64
65
  return result unless (klass = safe_assigned_class)
66
+ return result if GraphQL.type_map.exist?(klass, namespaces: namespaces)
65
67
 
66
68
  GraphQL.type_map.register_alias(klass, namespaces: namespaces, &method(:itself))
67
69
  result
@@ -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
- list = event_types if append
33
- list += list.flatten.compact.map(&:to_sym)
34
- @event_types = list.uniq.freeze
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
- event_types.each do |event_name|
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, include_private = false)
28
- (include_private ? %i[public protected private] : %i[public]).any? do |type|
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
- field?(:#{kind}, name)
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)
@@ -77,7 +77,7 @@ module Rails # :nodoc:
77
77
  # Get the GraphQL variables for a request
78
78
  def gql_variables(variables = params[:variables])
79
79
  case variables
80
- when ::ActionController::Parameters then variables.permit!
80
+ when ::ActionController::Parameters then variables.permit!.to_h
81
81
  when String then variables.present? ? JSON.parse(variables) : {}
82
82
  when Hash then variables
83
83
  else {}
@@ -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(field.all_events, super)
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(:dynamic_listeners)[field] ||= field.all_listeners
191
+ listeners = request.cache(:listeners)[field] ||= field.all_listeners
187
192
  return super unless listeners.include?(event_name)
188
193
 
189
- callbacks = request.cache(:dynamic_events)[field] ||= field.all_events
194
+ callbacks = request.cache(:events)[field] ||= field.all_events
190
195
  old_events, @all_events = @all_events, callbacks
191
196
  super
192
197
  ensure
@@ -44,9 +44,9 @@ module Rails # :nodoc:
44
44
 
45
45
  item['path'] = path if path.present? && path.is_a?(Array)
46
46
  item['extensions'] = extra.deep_stringify_keys if extra.present?
47
- item['locations'].map!(&:stringify_keys)
47
+ item['locations']&.map!(&:stringify_keys)
48
48
 
49
- @items << item
49
+ @items << item.compact
50
50
  end
51
51
  end
52
52
  end
@@ -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(klass, &block)
75
- set_on(klass.is_a?(Class) ? instance_for(klass) : klass, &block)
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 all directives
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 all directives and caches it by request
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
@@ -145,6 +145,11 @@ module Rails # :nodoc:
145
145
  defined?(@built) && !!@built
146
146
  end
147
147
 
148
+ # Checks if a given method can act as resolver
149
+ def gql_resolver?(method_name)
150
+ (instance_methods - GraphQL::Source.instance_methods).include?(method_name)
151
+ end
152
+
148
153
  # Attach all defined schema fields into the schemas using the namespaces
149
154
  # configured for the source
150
155
  def attach_fields!
@@ -91,7 +91,7 @@ module Rails
91
91
  def build_reflection_fields(holder)
92
92
  each_reflection(holder) do |item|
93
93
  next if holder.field?(item.name)
94
- type_map_after_register(item.klass.name) do |type|
94
+ type_map_after_register(item.klass) do |type|
95
95
  next unless (type.object? && type.try(:assigned_to) != item.klass) ||
96
96
  type.interface?
97
97
 
@@ -100,7 +100,7 @@ module Rails # :nodoc:
100
100
  next if model.base_class == model
101
101
 
102
102
  # TODO: Allow nested inheritance for setting up implementation
103
- type_map_after_register(model.base_class.name) do |type|
103
+ type_map_after_register(model.base_class) do |type|
104
104
  object.implements(type) if type.interface?
105
105
  end
106
106
  end
@@ -124,6 +124,11 @@ module Rails # :nodoc:
124
124
  @enums ||= model.defined_enums.dup
125
125
  end
126
126
 
127
+ # Just a little override to ensure that both model and table are ready
128
+ def build!
129
+ super if model&.table_exists?
130
+ end
131
+
127
132
  protected
128
133
 
129
134
  # Check if a given +attr_name+ is associated with a presence validator
@@ -145,13 +150,13 @@ module Rails # :nodoc:
145
150
  end
146
151
 
147
152
  # Prepare to load multiple records from the underlying table
148
- def load_records
149
- inject_scopes(model.all, :relation)
153
+ def load_records(scope = model.default_scoped)
154
+ inject_scopes(scope, :relation)
150
155
  end
151
156
 
152
157
  # Prepare to load a single record from the underlying table
153
- def load_record
154
- load_records.find(event.argument(primary_key))
158
+ def load_record(scope = model.default_scoped)
159
+ scope.find(event.argument(primary_key))
155
160
  end
156
161
 
157
162
  # Get the chain result and preload the records with thre resulting scope
@@ -161,7 +166,7 @@ module Rails # :nodoc:
161
166
 
162
167
  # Collect a scope for filters applied to a given association
163
168
  def build_association_scope(association)
164
- scope = model._reflect_on_association(association).klass.unscoped
169
+ scope = model._reflect_on_association(association).klass.default_scoped
165
170
 
166
171
  # Apply proxied injected scopes
167
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.merge(owner: self), &block)
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, *values|
227
- next unless @index.key?(values.last)
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[values.last][base_class]&.each do |_key, value|
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
@@ -252,7 +252,7 @@ module Rails # :nodoc:
252
252
  position = callbacks[name_or_key].size
253
253
 
254
254
  callbacks[name_or_key] << ->(n, b, result) do
255
- return unless b === base_class && namespaces.include?(n)
255
+ return unless b === base_class && (n === :base || namespaces.include?(n))
256
256
  block.call(result)
257
257
  position
258
258
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rails # :nodoc:
4
4
  module GraphQL # :nodoc:
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end
@@ -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.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-06 00:00:00.000000000 Z
11
+ date: 2021-02-16 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