hyper-mesh 1.0.0.lap23 → 1.0.0.lap24

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dd51d3374de90e1a070483b1c3db8fd002376dcede0a581e8bddf295605357f
4
- data.tar.gz: 374199c45836ce94e38ca0e4a7c82477ac21bf56c37b14294b4c1fff199dd7f8
3
+ metadata.gz: f71958166fcd1ea5b7b054c460a431f45ebae9389098d3046e805a38225235d6
4
+ data.tar.gz: 9bcf2cb4090bc56920db979d5e016fdcfa450497c375b8487efa8e94e73da837
5
5
  SHA512:
6
- metadata.gz: d56429c6e9b76916a9368b1e56550a1e3fa116224703e400160e1fb8f6ec84361cf8560c3d48b0d1ffd3bbd143e7427bc5d8d60fb325d9c9fd8ccd6f5a3f1192
7
- data.tar.gz: 3cdbcc9c488c9ddccb962ff1991b2c0c797bb59d772a59d0ef5237e5e1939afa56b7b8664d0285edf4391bbef9ffd1a290aac54150265ade8699a5ac2d6aa429
6
+ metadata.gz: 59e3c865aa085a2a16ee573d8dcd147d0dc96d91e04fa4799e29e3dc80cce5aba5801e837686a612c2ac72b15f42a21767c8250695552a882c24fd1bdd34fb41
7
+ data.tar.gz: aaa9fb686b480a3ad322e835fa7f8a95a13e37f4a508a374c94aa75585f12f03eb5053f4d33c819269a09f8864f27d67ed639f7146992e96f2c89555307e6d9a
data/DOCS.md CHANGED
@@ -617,6 +617,7 @@ Pusher.app_id = "MY_TEST_ID" # you use the real or fake values
617
617
  Pusher.key = "MY_TEST_KEY"
618
618
  Pusher.secret = "MY_TEST_SECRET"
619
619
  # The next line actually starts the pusher-fake server (see the Pusher-Fake readme for details.)
620
+ # it is important this require be AFTER the above settings, as it will use these
620
621
  require 'pusher-fake/support/base' # if using pusher with rspec change this to pusher-fake/support/rspec
621
622
  # now copy over the credentials, and merge with PusherFake's config details
622
623
  Hyperloop.configuration do |config|
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require "rspec/core/rake_task"
4
4
 
5
5
 
6
6
  task :spec do
7
- (1..5).each { |batch| Rake::Task["spec:batch#{batch}"].invoke }
7
+ (1..6).each { |batch| Rake::Task["spec:batch#{batch}"].invoke }
8
8
  end
9
9
 
10
10
  namespace :spec do
@@ -12,7 +12,7 @@ namespace :spec do
12
12
  sh %{bundle update}
13
13
  sh %{cd spec/test_app; bundle update; bundle exec rails db:setup} # may need ;bundle exec rails db:setup as well
14
14
  end
15
- (1..5).each do |batch|
15
+ (1..6).each do |batch|
16
16
  RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
17
17
  t.pattern = "spec/batch#{batch}/**/*_spec.rb"
18
18
  end
@@ -25,8 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.test_files = `git ls-files -- {spec}/*`.split("\n")
26
26
  spec.require_paths = ['lib']
27
27
 
28
- spec.post_install_message = "\033[0;31;1mhyper-mesh #{Hypermesh::VERSION} is in development and has known security issues! Not recommended for production use!\033[0;30;21m"
29
-
30
28
  spec.add_dependency 'activerecord', '>= 4.0.0'
31
29
  spec.add_dependency 'hyper-component', Hypermesh::VERSION
32
30
  spec.add_dependency 'hyper-operation', Hypermesh::VERSION
@@ -65,4 +63,5 @@ Gem::Specification.new do |spec|
65
63
  spec.add_development_dependency 'timecop', '~> 0.8.1'
66
64
  spec.add_development_dependency 'unparser'
67
65
  spec.add_development_dependency 'pry'
66
+ spec.add_development_dependency 'pry-rescue'
68
67
  end
@@ -1,131 +1,293 @@
1
+ # Monkey patches to ActiveRecord for scoping, security, and to synchronize models
1
2
  module ActiveRecord
2
- # ActiveRecord monkey patches
3
- # 1 - Setup synchronization after commits
4
- # 2 - Update scope to accept different procs for server and client
3
+ # hyperloop adds new features to scopes to allow for computing scopes on client side
4
+ # and for hinting at what joins are involved in a scope. _synchromesh_scope_args_check
5
+ # processes these arguments, and the will always leave the true server side scoping
6
+ # proc in the `:server` opts. This method is common to client and server.
5
7
  class Base
6
-
7
- class << self
8
-
9
- def _synchromesh_scope_args_check(args)
10
- opts = if args.count == 2 && args[1].is_a?(Hash)
11
- args[1].merge(server: args[0])
12
- elsif args[0].is_a? Hash
13
- args[0]
14
- else
15
- { server: args[0] }
16
- end
17
- return opts if opts.is_a?(Hash) && opts[:server].respond_to?(:call)
18
- raise 'must provide either a proc as the first arg or by the '\
19
- '`:server` option to scope and default_scope methods'
8
+ def self._synchromesh_scope_args_check(args)
9
+ opts = if args.count == 2 && args[1].is_a?(Hash)
10
+ args[1].merge(server: args[0])
11
+ elsif args[0].is_a? Hash
12
+ args[0]
13
+ else
14
+ { server: args[0] }
15
+ end
16
+ return opts if opts && opts[:server].respond_to?(:call)
17
+ raise 'must provide either a proc as the first arg or by the '\
18
+ '`:server` option to scope and default_scope methods'
19
+ end
20
+ end
21
+ if RUBY_ENGINE != 'opal'
22
+ # __synchromesh_permission_granted indicates if permission has been given to return a scope
23
+ # The acting_user attribute is set to the current acting_user so regulation methods can check it
24
+ # The __secure_collection_check method is called at the end of a scope chain and will fail if
25
+ # no scope in the chain has positively granted access.
26
+
27
+ # allows us to easily handle scopes and finder_methods which return arrays of items
28
+ # (instead of ActiveRecord::Relations - see below)
29
+ class ReactiveRecordPsuedoRelationArray < Array
30
+ attr_accessor :__synchromesh_permission_granted
31
+ attr_accessor :acting_user
32
+ def __secure_collection_check(_acting_user)
33
+ self
20
34
  end
35
+ end
21
36
 
22
- alias pre_synchromesh_scope scope
23
-
24
- def do_not_synchronize
25
- @do_not_synchronize = true
37
+ # add the __synchromesh_permission_granted, acting_user and __secure_collection_check
38
+ # methods to Relation
39
+ class Relation
40
+ attr_accessor :__synchromesh_permission_granted
41
+ attr_accessor :acting_user
42
+ def __secure_collection_check(acting_user)
43
+ return self if __synchromesh_permission_granted
44
+ return self if __secure_remote_access_to_unscoped(acting_user).__synchromesh_permission_granted
45
+ denied!
26
46
  end
47
+ end
48
+ # Monkey patches and extensions to base
49
+ class Base
50
+ class << self
51
+ # every method call that is legal from the client has a wrapper method prefixed with
52
+ # __secure_remote_access_to_
53
+
54
+ # The wrapper method may simply return the normal result or may act to secure the data.
55
+ # The simpliest case is for the method to call `denied!` which will raise a Hyperloop
56
+ # access protection fault.
57
+
58
+ def denied!
59
+ Hyperloop::InternalPolicy.raise_operation_access_violation
60
+ end
27
61
 
28
- def do_not_synchronize?
29
- @do_not_synchronize
30
- end
31
-
32
- if RUBY_ENGINE != 'opal'
33
-
34
- alias pre_synchromesh_default_scope default_scope
62
+ # Here we set up the base `all` and `unscoped` methods. See below for more on how
63
+ # access protection works on relationships.
35
64
 
36
- def scope(name, *args, &block)
37
- opts = _synchromesh_scope_args_check(args)
38
- pre_synchromesh_scope(name, opts[:server], &block)
65
+ def __secure_remote_access_to_all(_acting_user)
66
+ all
39
67
  end
40
68
 
41
- def default_scope(*args, &block)
42
- opts = _synchromesh_scope_args_check([*block, *args])
43
- pre_synchromesh_default_scope(opts[:server], &block)
69
+ def __secure_remote_access_to_unscoped(_acting_user, *args)
70
+ unscoped(*args)
44
71
  end
45
72
 
46
- def server_method(name, opts = {}, &block)
47
- define_method(name, &block)
48
- end
73
+ # finder_method and server_method provide secure RPCs against AR relations and records.
74
+ # The block is called in context with the object, and acting_user is set to the
75
+ # current acting user. The block may interogate acting_user to insure security as needed.
76
+
77
+ # For finder_method we have to preapply `all` so that we always have a relationship
49
78
 
50
79
  def finder_method(name, &block)
51
- singleton_class.send(:define_method, "_#{name}") do |*args|
52
- [block.call(*args)]
80
+ singleton_class.send(:define_method, :"__secure_remote_access_to__#{name}") do |acting_user, *args|
81
+ this = respond_to?(:acting_user) ? self : all
82
+ begin
83
+ old = this.acting_user
84
+ this.acting_user = acting_user
85
+ # returns a PsuedoRelationArray which will respond to the
86
+ # __secure_collection_check method
87
+ ReactiveRecordPsuedoRelationArray.new([this.instance_exec(*args, &block)])
88
+ ensure
89
+ this.acting_user = old
90
+ end
53
91
  end
54
92
  singleton_class.send(:define_method, name) do |*args|
55
- block.call(*args)
93
+ all.instance_exec(*args, &block)
56
94
  end
57
95
  end
58
96
 
59
- else
97
+ def server_method(name, _opts = {}, &block)
98
+ # callable from the server internally
99
+ define_method(name, &block)
100
+ # callable remotely from the client
101
+ define_method("__secure_remote_access_to_#{name}") do |acting_user, *args|
102
+ begin
103
+ old = self.acting_user
104
+ self.acting_user = acting_user
105
+ send(name, *args)
106
+ ensure
107
+ self.acting_user = old
108
+ end
109
+ end
110
+ end
60
111
 
61
- alias pre_synchromesh_method_missing method_missing
112
+ # relationships (and scopes) are regulated using a tri-state system. Each
113
+ # remote access method will return the relationship as normal but will also set
114
+ # the value of __secure_remote_access_granted using the application defined regulation.
115
+ # Each regulation can explicitly allow the scope to be chained by returning a truthy
116
+ # value from the regulation. Or each regulation can explicitly deny the scope to
117
+ # be chained by called `denied!`. Otherwise each regulation can return a falsy
118
+ # value meaning the scope can be changed, but unless some other scope (before or
119
+ # after) in the chain explicitly allows the scope, the entire chain will fail.
120
+
121
+ # In otherwords within a chain of relationships and scopes, at least one Regulation
122
+ # must be return a truthy value otherwise the whole chain fails. Likewise if any
123
+ # regulation called `deined!` the whole chain fails.
124
+
125
+ # If no regulation is defined, the regulation is inherited from the superclass, and if
126
+ # no regulation is defined anywhere in the class heirarchy then the regulation will
127
+ # return a falsy value.
128
+
129
+ # regulations on scopes are inheritable. That is if a superclass defines a regulation
130
+ # for a scope, subclasses will inherit the regulation (but can override)
131
+
132
+ # helper method to sort out the options on the regulate_scope, regulate_relationship macros.
133
+
134
+ # We allow three forms:
135
+ # regulate_xxx name &block : the block is the regulation
136
+ # regulate_xxx name: const : const can be denied!, deny, denied, or any other truthy or
137
+ # falsy value
138
+ # regulate_xxx name: proc : the proc is the regulation
139
+
140
+ def __synchromesh_parse_regulator_params(name, block)
141
+ if name.is_a? Hash
142
+ name, block = name.first
143
+ if %i[denied! deny denied].include? block
144
+ block = ->(*_args) { denied! }
145
+ elsif !block.is_a? Proc
146
+ value = block
147
+ block = ->(*_args) { value }
148
+ end
149
+ end
150
+ [name, block || ->(*_args) { true }]
151
+ end
62
152
 
63
- def method_missing(name, *args, &block)
64
- #return get_by_index(*args).first if name == "[]"
65
- return all.send(name, *args, &block) if [].respond_to?(name)
66
- if name.end_with?('!')
67
- return send(name.chop, *args, &block).send(:reload_from_db) rescue nil
153
+ # helper method for providing a regulation in line with a scope or relationship
154
+ # this is done using the `regulate` key on the opts.
155
+ # if no regulate key is provided and there is no regulation already defined for
156
+ # this name, then we create one that returns nil (don't care)
157
+ # once we have things figured out, we yield to the provided proc which is either
158
+ # regulate_scope or regulate_relationship
159
+
160
+ def __synchromesh_regulate_from_macro(opts, name, already_defined)
161
+ if opts.key?(:regulate)
162
+ yield name => opts[:regulate]
163
+ elsif !already_defined
164
+ yield name => ->(*_args) {}
68
165
  end
69
- pre_synchromesh_method_missing(name, *args, &block)
70
166
  end
71
167
 
72
- def create(*args, &block)
73
- new(*args).save(&block)
168
+ # helper method to set the value of __synchromesh_permission_granted on the relationship
169
+ # Get the current value of __synchromesh_permission_granted, set acting_user on the
170
+ # object, and or in the result of running the block in context of the obj
171
+
172
+ def __set_synchromesh_permission_granted(r, obj, acting_user, args = [], &block)
173
+ r.__synchromesh_permission_granted = try(:__synchromesh_permission_granted)
174
+ old = acting_user
175
+ obj.acting_user = acting_user
176
+ r.__synchromesh_permission_granted ||= obj.instance_exec(*args, &block)
177
+ r
178
+ ensure
179
+ obj.acting_user = old
74
180
  end
75
181
 
76
- def scope(name, *args)
77
- opts = _synchromesh_scope_args_check(args)
78
- scope_description = ReactiveRecord::ScopeDescription.new(self, name, opts)
79
- singleton_class.send(:define_method, name) do |*vargs|
80
- all.build_child_scope(scope_description, *name, *vargs)
81
- end
82
- singleton_class.send(:define_method, "#{name}=") do |_collection|
83
- raise 'NO LONGER IMPLEMENTED - DOESNT PLAY WELL WITH SYNCHROMESH'
84
- # all.replace_child_scope(name, collection)
182
+ # regulate scope has to deal with the special case that the scope returns an
183
+ # an array instead of a relationship. In this case we wrap the array and go on
184
+
185
+ def regulate_scope(name, &block)
186
+ name, block = __synchromesh_parse_regulator_params(name, block)
187
+ singleton_class.send(:define_method, :"__secure_remote_access_to_#{name}") do |acting_user, *args|
188
+ r = send(name, *args)
189
+ r = ReactiveRecordPsuedoRelationArray.new(r) if r.is_a? Array
190
+ __set_synchromesh_permission_granted(r, r, acting_user, args, &block)
85
191
  end
86
192
  end
87
193
 
194
+ # regulate_default_scope
195
+
196
+ def regulate_default_scope(&block)
197
+ regulate_scope(:all, &block)
198
+ end
199
+
200
+ # monkey patch scope and default_scope macros to process hyperloop special opts,
201
+ # and add regulations if present
202
+
203
+ alias pre_synchromesh_scope scope
204
+
205
+ def scope(name, *args, &block)
206
+ __synchromesh_regulate_from_macro(
207
+ (opts = _synchromesh_scope_args_check(args)),
208
+ name,
209
+ respond_to?(:"__secure_remote_access_to_#{name}"),
210
+ &method(:regulate_scope)
211
+ )
212
+ pre_synchromesh_scope(name, opts[:server], &block)
213
+ end
214
+
215
+ alias pre_synchromesh_default_scope default_scope
216
+
88
217
  def default_scope(*args, &block)
89
- opts = _synchromesh_scope_args_check([*block, *args])
90
- @_default_scopes ||= []
91
- @_default_scopes << opts
218
+ __synchromesh_regulate_from_macro(
219
+ (opts = _synchromesh_scope_args_check([*block, *args])),
220
+ :all,
221
+ respond_to?(:__secure_remote_access_to_all),
222
+ &method(:regulate_scope)
223
+ )
224
+ pre_synchromesh_default_scope(opts[:server], &block)
92
225
  end
93
226
 
94
- def all
95
- ReactiveRecord::Base.default_scope[self] ||=
96
- if @_default_scopes
97
- root = ReactiveRecord::Collection
98
- .new(self, nil, nil, self, 'all')
99
- .extend(ReactiveRecord::UnscopedCollection)
100
- @_default_scopes.inject(root) do |scope, opts|
101
- scope.build_child_scope(ReactiveRecord::ScopeDescription.new(self, :all, opts))
102
- end
103
- end || unscoped
227
+ # add regulate_relationship method and monkey patch monkey patch has_many macro
228
+ # to add regulations if present
229
+
230
+ def regulate_relationship(name, &block)
231
+ name, block = __synchromesh_parse_regulator_params(name, block)
232
+ define_method(:"__secure_remote_access_to_#{name}") do |acting_user, *args|
233
+ self.class.__set_synchromesh_permission_granted(
234
+ send(name, *args), self, acting_user, &block
235
+ )
236
+ end
237
+ end
238
+
239
+ alias pre_syncromesh_has_many has_many
240
+
241
+ def has_many(name, *args, &block)
242
+ __synchromesh_regulate_from_macro(
243
+ opts = args.extract_options!,
244
+ name,
245
+ method_defined?(:"__secure_remote_access_to_#{name}"),
246
+ &method(:regulate_relationship)
247
+ )
248
+ pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
104
249
  end
105
250
 
106
- def all=(_collection)
107
- raise "NO LONGER IMPLEMENTED DOESNT PLAY WELL WITH SYNCHROMESH"
251
+ # add secure access for find, find_by, and belongs_to and has_one relations.
252
+ # No explicit security checks are needed here, as the data returned by these objects
253
+ # will be further processedand checked before returning. I.e. it is not possible to
254
+ # simply return `find(1)` but if you try returning `find(1).name` the permission system
255
+ # will check to see if the name attribute can be legally sent to the current acting user.
256
+
257
+ def __secure_remote_access_to_find(_acting_user, *args)
258
+ find(*args)
108
259
  end
109
260
 
110
- def unscoped
111
- ReactiveRecord::Base.unscoped[self] ||=
112
- ReactiveRecord::Collection
113
- .new(self, nil, nil, self, 'unscoped')
114
- .extend(ReactiveRecord::UnscopedCollection)
261
+ def __secure_remote_access_to_find_by(_acting_user, *args)
262
+ find_by(*args)
115
263
  end
116
264
 
117
- def finder_method(name)
118
- ReactiveRecord::ScopeDescription.new(self, "_#{name}", {})
119
- [name, "#{name}!"].each do |method|
120
- singleton_class.send(:define_method, method) do |*vargs|
121
- all.apply_scope("_#{method}", *vargs).first
265
+ %i[belongs_to has_one].each do |macro|
266
+ alias_method :"pre_syncromesh_#{macro}", macro
267
+ define_method(macro) do |name, scope = nil, opts = {}, &block|
268
+ define_method(:"__secure_remote_access_to_#{name}") do |_acting_user, *args|
269
+ send(name, *args)
122
270
  end
271
+ send(:"pre_syncromesh_#{macro}", name, scope, opts, &block)
123
272
  end
124
273
  end
125
274
  end
126
- end
127
275
 
128
- if RUBY_ENGINE != 'opal'
276
+ def denied!
277
+ Hyperloop::InternalPolicy.raise_operation_access_violation
278
+ end
279
+
280
+ # call do_not_synchronize to block synchronization of a model
281
+
282
+ def self.do_not_synchronize
283
+ @do_not_synchronize = true
284
+ end
285
+
286
+ # used by the broadcast mechanism to determine if this model is to be synchronized
287
+
288
+ def self.do_not_synchronize?
289
+ @do_not_synchronize
290
+ end
129
291
 
130
292
  def do_not_synchronize?
131
293
  self.class.do_not_synchronize?
@@ -136,7 +298,7 @@ module ActiveRecord
136
298
  after_commit :synchromesh_after_destroy, on: [:destroy]
137
299
 
138
300
  def synchromesh_after_create
139
- return if do_not_synchronize? #|| previous_changes.empty?
301
+ return if do_not_synchronize?
140
302
  ReactiveRecord::Broadcast.after_commit :create, self
141
303
  end
142
304
 
@@ -149,27 +311,8 @@ module ActiveRecord
149
311
  return if do_not_synchronize?
150
312
  ReactiveRecord::Broadcast.after_commit :destroy, self
151
313
  end
152
- else
153
-
154
- scope :limit, ->() {}
155
- scope :offset, ->() {}
156
-
157
- def update_attribute(attr, value, &block)
158
- send("#{attr}=", value)
159
- save(validate: false, &block)
160
- end
161
-
162
- def update(attrs = {}, &block)
163
- attrs.each { |attr, value| send("#{attr}=", value) }
164
- save(&block)
165
- end
166
-
167
- def <=>(other)
168
- id.to_i <=> other.id.to_i
169
- end
170
314
  end
171
315
  end
172
316
 
173
317
  InternalMetadata.do_not_synchronize if defined? InternalMetadata
174
-
175
318
  end
@@ -22,12 +22,12 @@ if RUBY_ENGINE == 'opal'
22
22
  require "reactive_record/active_record/reactive_record/collection"
23
23
  require "reactive_record/active_record/reactive_record/scoped_collection"
24
24
  require "reactive_record/active_record/reactive_record/unscoped_collection"
25
+ require "reactive_record/interval"
26
+ require_relative 'active_record_base'
27
+ require_relative 'reactive_record/scope_description'
25
28
  require "reactive_record/active_record/class_methods"
26
29
  require "reactive_record/active_record/instance_methods"
27
30
  require "reactive_record/active_record/base"
28
- require "reactive_record/interval"
29
- require_relative 'reactive_record/scope_description'
30
- require_relative 'active_record_base'
31
31
  require_relative 'hypermesh/version'
32
32
  require_relative 'opal/parse_patch'
33
33
  require_relative 'opal/set_patches'
@@ -1,3 +1,3 @@
1
1
  module Hypermesh
2
- VERSION = '1.0.0.lap23'
2
+ VERSION = '1.0.0.lap24'
3
3
  end
@@ -1,9 +1,11 @@
1
1
  module ActiveRecord
2
+ # client side ActiveRecord::Base proxy
2
3
  class Base
3
-
4
4
  extend ClassMethods
5
5
 
6
6
  include InstanceMethods
7
7
 
8
+ scope :limit, ->() {}
9
+ scope :offset, ->() {}
8
10
  end
9
11
  end
@@ -60,6 +60,7 @@ module ActiveRecord
60
60
  # ignore any of these methods if they get called on the client. This list should be trimmed down to include only
61
61
  # methods to be called as "macros" such as :after_create, etc...
62
62
  SERVER_METHODS = [
63
+ :regulate_relationship, :regulate_scope,
63
64
  :attribute_type_decorations, :defined_enums, :_validators, :timestamped_migrations, :lock_optimistically, :lock_optimistically=,
64
65
  :local_stored_attributes=, :lock_optimistically?, :attribute_aliases?, :attribute_method_matchers?, :defined_enums?,
65
66
  :has_many_without_reactive_record_add_changed_method, :has_many_with_reactive_record_add_changed_method,
@@ -78,7 +79,7 @@ module ActiveRecord
78
79
  :_find_callbacks, :_find_callbacks?, :_find_callbacks=, :_touch_callbacks, :_touch_callbacks?, :_touch_callbacks=, :_save_callbacks,
79
80
  :_save_callbacks?, :_save_callbacks=, :_create_callbacks, :_create_callbacks?, :_create_callbacks=, :_update_callbacks,
80
81
  :_update_callbacks?, :_update_callbacks=, :_destroy_callbacks, :_destroy_callbacks?, :_destroy_callbacks=, :record_timestamps?,
81
- :_synchromesh_scope_args_check, :pre_synchromesh_scope, :pre_synchromesh_default_scope, :do_not_synchronize, :do_not_synchronize?,
82
+ :pre_synchromesh_scope, :pre_synchromesh_default_scope, :do_not_synchronize, :do_not_synchronize?,
82
83
  :logger=, :maintain_test_schema, :maintain_test_schema=, :scope, :time_zone_aware_attributes, :time_zone_aware_attributes=,
83
84
  :default_timezone, :default_timezone=, :_attr_readonly, :warn_on_records_fetched_greater_than, :configurations, :configurations=,
84
85
  :_attr_readonly?, :table_name_prefix=, :table_name_suffix=, :schema_migrations_table_name=, :internal_metadata_table_name,
@@ -145,33 +146,107 @@ module ActiveRecord
145
146
  def method_missing(name, *args, &block)
146
147
  if args.count == 1 && name.start_with?("find_by_") && !block
147
148
  find_by(name.sub(/^find_by_/, "") => args[0])
149
+ elsif [].respond_to?(name)
150
+ all.send(name, *args, &block)
151
+ elsif name.end_with?('!')
152
+ send(name.chop, *args, &block).send(:reload_from_db) rescue nil
148
153
  elsif !SERVER_METHODS.include?(name)
149
154
  raise "#{self.name}.#{name}(#{args}) (called class method missing)"
150
155
  end
151
156
  end
152
157
 
153
- def abstract_class=(val)
154
- @abstract_class = val
158
+ # client side AR
159
+
160
+ # Any method that can be applied to an array will be applied to the result
161
+ # of all instead.
162
+ # Any method ending with ! just means apply the method after forcing a reload
163
+ # from the DB.
164
+
165
+ # alias pre_synchromesh_method_missing method_missing
166
+ #
167
+ # def method_missing(name, *args, &block)
168
+ # return all.send(name, *args, &block) if [].respond_to?(name)
169
+ # if name.end_with?('!')
170
+ # return send(name.chop, *args, &block).send(:reload_from_db) rescue nil
171
+ # end
172
+ # pre_synchromesh_method_missing(name, *args, &block)
173
+ # end
174
+
175
+ def create(*args, &block)
176
+ new(*args).save(&block)
155
177
  end
156
178
 
157
- def scope(name, body)
158
- singleton_class.send(:define_method, name) do | *args |
159
- args = (args.count == 0) ? name : [name, *args]
160
- ReactiveRecord::Base.class_scopes(self)[args] ||= ReactiveRecord::Collection.new(self, nil, nil, self, args)
161
- end
162
- singleton_class.send(:define_method, "#{name}=") do |collection|
163
- ReactiveRecord::Base.class_scopes(self)[name] = collection
179
+ def scope(name, *args)
180
+ opts = _synchromesh_scope_args_check(args)
181
+ scope_description = ReactiveRecord::ScopeDescription.new(self, name, opts)
182
+ singleton_class.send(:define_method, name) do |*vargs|
183
+ all.build_child_scope(scope_description, *name, *vargs)
164
184
  end
185
+ # singleton_class.send(:define_method, "#{name}=") do |_collection|
186
+ # raise 'NO LONGER IMPLEMENTED - DOESNT PLAY WELL WITH SYNCHROMESH'
187
+ # end
188
+ end
189
+
190
+ def default_scope(*args, &block)
191
+ opts = _synchromesh_scope_args_check([*block, *args])
192
+ @_default_scopes ||= []
193
+ @_default_scopes << opts
165
194
  end
166
195
 
167
196
  def all
168
- ReactiveRecord::Base.class_scopes(self)[:all] ||= ReactiveRecord::Collection.new(self, nil, nil, self, "all")
197
+ ReactiveRecord::Base.default_scope[self] ||=
198
+ begin
199
+ root = ReactiveRecord::Collection
200
+ .new(self, nil, nil, self, 'all')
201
+ .extend(ReactiveRecord::UnscopedCollection)
202
+ (@_default_scopes || [{ client: -> () { true } }]).inject(root) do |scope, opts|
203
+ scope.build_child_scope(ReactiveRecord::ScopeDescription.new(self, :all, opts))
204
+ end
205
+ end
169
206
  end
170
207
 
171
- def all=(collection)
172
- ReactiveRecord::Base.class_scopes(self)[:all] = collection
208
+ # def all=(_collection)
209
+ # raise "NO LONGER IMPLEMENTED DOESNT PLAY WELL WITH SYNCHROMESH"
210
+ # end
211
+
212
+ def unscoped
213
+ ReactiveRecord::Base.unscoped[self] ||=
214
+ ReactiveRecord::Collection
215
+ .new(self, nil, nil, self, 'unscoped')
216
+ .extend(ReactiveRecord::UnscopedCollection)
173
217
  end
174
218
 
219
+ def finder_method(name)
220
+ ReactiveRecord::ScopeDescription.new(self, "_#{name}", {})
221
+ [name, "#{name}!"].each do |method|
222
+ singleton_class.send(:define_method, method) do |*vargs|
223
+ all.apply_scope("_#{method}", *vargs).first
224
+ end
225
+ end
226
+ end
227
+
228
+ def abstract_class=(val)
229
+ @abstract_class = val
230
+ end
231
+
232
+ # def scope(name, body)
233
+ # singleton_class.send(:define_method, name) do | *args |
234
+ # args = (args.count == 0) ? name : [name, *args]
235
+ # ReactiveRecord::Base.class_scopes(self)[args] ||= ReactiveRecord::Collection.new(self, nil, nil, self, args)
236
+ # end
237
+ # singleton_class.send(:define_method, "#{name}=") do |collection|
238
+ # ReactiveRecord::Base.class_scopes(self)[name] = collection
239
+ # end
240
+ # end
241
+
242
+ # def all
243
+ # ReactiveRecord::Base.class_scopes(self)[:all] ||= ReactiveRecord::Collection.new(self, nil, nil, self, "all")
244
+ # end
245
+ #
246
+ # def all=(collection)
247
+ # ReactiveRecord::Base.class_scopes(self)[:all] = collection
248
+ # end
249
+
175
250
  [:belongs_to, :has_many, :has_one].each do |macro|
176
251
  define_method(macro) do |*args| # is this a bug in opal? saying name, scope=nil, opts={} does not work!
177
252
  name = args.first
@@ -116,7 +116,7 @@ module ActiveRecord
116
116
  # for rails auto generated methods for booleans, remove '?' to get the attribute
117
117
  name = name.chop if !is_server_method && is_attribute && name.end_with?('?')
118
118
  @backing_record.reactive_get!(name, force_update)
119
- elsif !block
119
+ elsif !block
120
120
  # for rails auto generated methods for booleans, remove '?' to get the attribute
121
121
  name = name.chop if !is_server_method && is_attribute && name.end_with?('?')
122
122
  @backing_record.reactive_get!([[name]+args], force_update)
@@ -167,6 +167,20 @@ module ActiveRecord
167
167
  @backing_record.errors
168
168
  end
169
169
 
170
+ def update_attribute(attr, value, &block)
171
+ send("#{attr}=", value)
172
+ save(validate: false, &block)
173
+ end
174
+
175
+ def update(attrs = {}, &block)
176
+ attrs.each { |attr, value| send("#{attr}=", value) }
177
+ save(&block)
178
+ end
179
+
180
+ def <=>(other)
181
+ id.to_i <=> other.id.to_i
182
+ end
183
+
170
184
  end
171
185
 
172
186
  end
@@ -87,7 +87,7 @@ module ReactiveRecord
87
87
  record = @records[model].detect { |record| record.id == id}
88
88
  end
89
89
  # if we don't have a record then create one
90
- (record = new(model)).vector = [model, ["find_by_#{attribute}", value]] unless record
90
+ (record = new(model)).vector = [model, [:find_by, attribute => value]] unless record
91
91
  # and set the value
92
92
  record.sync_attribute(attribute, value)
93
93
  # and set the primary if we have one
@@ -263,7 +263,7 @@ module ReactiveRecord
263
263
  # nil must not have any children.
264
264
  def initialize_collections
265
265
  if (!vector || vector.empty?) && id && id != ''
266
- @vector = [@model, ["find_by_#{@model.primary_key}", id]]
266
+ @vector = [@model, [:find_by, @model.primary_key => id]]
267
267
  end
268
268
  @model.reflect_on_all_associations.each do |assoc|
269
269
  if assoc.collection? && attributes[assoc.attribute].nil?
@@ -252,7 +252,7 @@ module ReactiveRecord
252
252
  elsif filter?
253
253
  @collection = filter_records(@parent.collection)
254
254
  end
255
- elsif @parent.count.zero?
255
+ elsif @parent._count_internal(false).zero? # just changed this from count.zero?
256
256
  @count = 0
257
257
  end
258
258
  end
@@ -289,18 +289,26 @@ module ReactiveRecord
289
289
  @count = val
290
290
  end
291
291
 
292
- def count
292
+
293
+
294
+ def _count_internal(load_from_client)
295
+ # when count is called on a leaf, count_internal is called for each
296
+ # ancestor. Only the outermost count has load_from_client == true
293
297
  observed
294
298
  if @collection
295
299
  @collection.count
296
300
  elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
297
301
  @count
298
302
  else
299
- ReactiveRecord::Base.load_from_db(nil, *@vector, "*count")
303
+ ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") if load_from_client
300
304
  @count = 1
301
305
  end
302
306
  end
303
307
 
308
+ def count
309
+ _count_internal(true)
310
+ end
311
+
304
312
  alias_method :length, :count
305
313
 
306
314
  # WHY IS THIS NEEDED? Perhaps it was just for debug
@@ -473,15 +481,19 @@ module ReactiveRecord
473
481
  @dummy_collection.loading?
474
482
  end
475
483
 
476
- def empty? # should be handled by method missing below, but opal-rspec does not deal well with method missing, so to test...
484
+ def empty?
485
+ # should be handled by method missing below, but opal-rspec does not deal well
486
+ # with method missing, so to test...
477
487
  all.empty?
478
488
  end
479
489
 
480
490
  def method_missing(method, *args, &block)
481
491
  if [].respond_to? method
482
492
  all.send(method, *args, &block)
483
- elsif ScopeDescription.find(@target_klass, method) || (args.count == 1 && method.start_with?("find_by_"))
493
+ elsif ScopeDescription.find(@target_klass, method)
484
494
  apply_scope(method, *args)
495
+ elsif args.count == 1 && method.start_with?('find_by_')
496
+ apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
485
497
  elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
486
498
  apply_scope("_#{method}", *args).first
487
499
  else
@@ -133,6 +133,7 @@ module ReactiveRecord
133
133
  end
134
134
 
135
135
  def self.schedule_fetch
136
+ React::State.set_state(WhileLoading, :quiet, false) # moved from while loading module see loading! method
136
137
  @fetch_scheduled ||= after(0) do
137
138
  if @pending_fetches.count > 0 # during testing we might reset the context while there are pending fetches otherwise this would never normally happen
138
139
  last_fetch_at = @last_fetch_at
@@ -143,23 +144,29 @@ module ReactiveRecord
143
144
  start_time = `Date.now()`
144
145
  Operations::Fetch.run(models: models, associations: associations, pending_fetches: pending_fetches)
145
146
  .then do |response|
146
- fetch_time = `Date.now()`
147
- log(" Fetched in: #{`(fetch_time - start_time)/ 1000`}s")
148
147
  begin
149
- ReactiveRecord::Base.load_from_json(response)
150
- rescue Exception => e
151
- log("Unexpected exception raised while loading json from server: #{e}", :error)
148
+ fetch_time = `Date.now()`
149
+ log(" Fetched in: #{`(fetch_time - start_time)/ 1000`}s")
150
+ begin
151
+ ReactiveRecord::Base.load_from_json(response)
152
+ rescue Exception => e
153
+ log("Unexpected exception raised while loading json from server: #{e}", :error)
154
+ end
155
+ log(" Processed in: #{`(Date.now() - fetch_time) / 1000`}s")
156
+ log([" Returned: %o", response.to_n])
157
+ ReactiveRecord.run_blocks_to_load last_fetch_at
158
+ ensure
159
+ ReactiveRecord::WhileLoading.loaded_at last_fetch_at
160
+ ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
152
161
  end
153
- log(" Processed in: #{`(Date.now() - fetch_time) / 1000`}s")
154
- log([" Returned: %o", response.to_n])
155
- ReactiveRecord.run_blocks_to_load last_fetch_at
156
- ReactiveRecord::WhileLoading.loaded_at last_fetch_at
157
- ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
158
162
  end
159
163
  .fail do |response|
160
164
  log("Fetch failed", :error)
161
- # not sure what response was supposed to look like here was response.body before conversion to operations....
162
- ReactiveRecord.run_blocks_to_load(last_fetch_at, response)
165
+ begin
166
+ ReactiveRecord.run_blocks_to_load(last_fetch_at, response)
167
+ ensure
168
+ ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
169
+ end
163
170
  end
164
171
  @pending_fetches = []
165
172
  @pending_records = []
@@ -123,7 +123,8 @@ module ReactiveRecord
123
123
  def loading!
124
124
  React::RenderingContext.waiting_on_resources = true
125
125
  React::State.get_state(self, :loaded_at)
126
- React::State.set_state(self, :quiet, false)
126
+ # this was moved to where the fetch is actually pushed on to the fetch array in isomorphic base
127
+ # React::State.set_state(self, :quiet, false)
127
128
  @is_loading = true
128
129
  end
129
130
 
@@ -298,7 +299,7 @@ if RUBY_ENGINE == 'opal'
298
299
  def reactive_record_link_set_while_loading_container_class
299
300
  node = dom_node
300
301
  loading = (waiting_on_resources ? `true` : `false`)
301
- %x{
302
+ %x{
302
303
  if (typeof node === "undefined" || node === null) return;
303
304
  var while_loading_container_id = node.getAttribute('data-reactive_record_while_loading_container_id');
304
305
  if (#{!self.is_a?(ReactiveRecord::WhileLoading)} && while_loading_container_id !== null && while_loading_container_id !== "") {
@@ -203,29 +203,36 @@ module ReactiveRecord
203
203
  def apply_method_to_cache(method)
204
204
  @db_cache.inject(nil) do |representative, cache_item|
205
205
  if cache_item.vector == vector
206
- # TODO: Security - this is the wrong check in the wrong place...
207
- if @value.class < ActiveRecord::Base and @value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
208
- @value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
209
- end
210
206
  if method == "*"
207
+ # apply_star does the security check if value is present
211
208
  cache_item.apply_star || representative
212
209
  elsif method == "*all"
213
- cache_item.build_new_cache_item(cache_item.value.collect { |record| record.id }, method, method)
210
+ # if we secure the collection then we assume its okay to read the ids
211
+ cache_item.build_new_cache_item(cache_item.value.__secure_collection_check(@acting_user).collect { |record| record.id }, method, method)
214
212
  elsif method == "*count"
215
- cache_item.build_new_cache_item(cache_item.value.count, method, method)
213
+ cache_item.build_new_cache_item(cache_item.value.__secure_collection_check(@acting_user).count, method, method)
216
214
  elsif preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
215
+ # no security check needed since we already evaluated this
217
216
  cache_item.build_new_cache_item(preloaded_value, method, method)
218
217
  elsif aggregation = cache_item.aggregation?(method)
218
+ # aggregations are not protected
219
219
  cache_item.build_new_cache_item(aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }, method, method)
220
220
  else
221
- if !cache_item.value || cache_item.value.class == Array
221
+ if !cache_item.value || cache_item.value.is_a?(Array)
222
+ # seeing as we just returning representative, no check is needed (its already checked)
222
223
  representative
223
224
  else
224
- # TODO: Security. Protect the send(*method). But its complicated.. method can be an attribute, scope, relationship or actual method.
225
- # Each needs some protection logic.
226
225
  begin
227
- cache_item.build_new_cache_item(cache_item.value.send(*method), method, method)
228
- rescue Exception => e
226
+ secured_method = "__secure_remote_access_to_#{[*method].first}"
227
+ if @value.class < ActiveRecord::Base and @value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
228
+ @value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
229
+ cache_item.build_new_cache_item(cache_item.value.send(*method), method, method)
230
+ elsif cache_item.value.respond_to? secured_method
231
+ cache_item.build_new_cache_item(cache_item.value.send(secured_method, @acting_user, *([*method][1..-1])), method, method)
232
+ else
233
+ raise "method missing"
234
+ end
235
+ rescue Exception => e # this check may no longer be needed as we are quite explicit now on which methods we apply
229
236
  # ReactiveRecord::Pry::rescued(e)
230
237
  ::Rails.logger.debug "\033[0;31;1mERROR: HyperModel exception caught when applying #{method} to db object #{cache_item.value}: #{e}\033[0;30;21m"
231
238
  raise e, "HyperModel fetching records failed, exception caught when applying #{method} to db object #{cache_item.value}: #{e}", e.backtrace
@@ -248,9 +255,9 @@ module ReactiveRecord
248
255
  end
249
256
  end
250
257
 
251
- # SECURITY - SAFE
258
+ # SECURITY - NOW SAFE
252
259
  def apply_star
253
- if @value && @value.length > 0
260
+ if @value && @value.__secure_collection_check(@acting_user) && @value.length > 0
254
261
  i = -1
255
262
  @value.inject(nil) do |representative, current_value|
256
263
  i += 1
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyper-mesh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.lap23
4
+ version: 1.0.0.lap24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitch VanDuyn
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-02-18 00:00:00.000000000 Z
12
+ date: 2018-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -31,28 +31,28 @@ dependencies:
31
31
  requirements:
32
32
  - - '='
33
33
  - !ruby/object:Gem::Version
34
- version: 1.0.0.lap23
34
+ version: 1.0.0.lap24
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - '='
40
40
  - !ruby/object:Gem::Version
41
- version: 1.0.0.lap23
41
+ version: 1.0.0.lap24
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: hyper-operation
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - '='
47
47
  - !ruby/object:Gem::Version
48
- version: 1.0.0.lap23
48
+ version: 1.0.0.lap24
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - '='
54
54
  - !ruby/object:Gem::Version
55
- version: 1.0.0.lap23
55
+ version: 1.0.0.lap24
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: bundler
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -129,14 +129,14 @@ dependencies:
129
129
  requirements:
130
130
  - - '='
131
131
  - !ruby/object:Gem::Version
132
- version: 1.0.0.lap23
132
+ version: 1.0.0.lap24
133
133
  type: :development
134
134
  prerelease: false
135
135
  version_requirements: !ruby/object:Gem::Requirement
136
136
  requirements:
137
137
  - - '='
138
138
  - !ruby/object:Gem::Version
139
- version: 1.0.0.lap23
139
+ version: 1.0.0.lap24
140
140
  - !ruby/object:Gem::Dependency
141
141
  name: hyper-trace
142
142
  requirement: !ruby/object:Gem::Requirement
@@ -549,6 +549,20 @@ dependencies:
549
549
  - - ">="
550
550
  - !ruby/object:Gem::Version
551
551
  version: '0'
552
+ - !ruby/object:Gem::Dependency
553
+ name: pry-rescue
554
+ requirement: !ruby/object:Gem::Requirement
555
+ requirements:
556
+ - - ">="
557
+ - !ruby/object:Gem::Version
558
+ version: '0'
559
+ type: :development
560
+ prerelease: false
561
+ version_requirements: !ruby/object:Gem::Requirement
562
+ requirements:
563
+ - - ">="
564
+ - !ruby/object:Gem::Version
565
+ version: '0'
552
566
  description: HyperMesh is the base for HyperModel. HyperModel gives your HyperComponents
553
567
  CRUD access to your ActiveRecord models on the client, using the the standard ActiveRecord
554
568
  API. HyperModel also implements push notifications (via a number of possible technologies)
@@ -622,8 +636,7 @@ homepage: http://ruby-hyperloop.org
622
636
  licenses:
623
637
  - MIT
624
638
  metadata: {}
625
- post_install_message: "\e[0;31;1mhyper-mesh 1.0.0.lap23 is in development and has
626
- known security issues! Not recommended for production use!\e[0;30;21m"
639
+ post_install_message:
627
640
  rdoc_options: []
628
641
  require_paths:
629
642
  - lib