shamu 0.0.21 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/Gemfile +3 -1
  5. data/Gemfile.lock +70 -69
  6. data/app/README +1 -0
  7. data/config/environment.rb +1 -0
  8. data/lib/shamu/attributes.rb +26 -0
  9. data/lib/shamu/attributes/equality.rb +10 -1
  10. data/lib/shamu/entities/entity.rb +11 -0
  11. data/lib/shamu/entities/list_scope.rb +3 -0
  12. data/lib/shamu/entities/list_scope/sorting.rb +6 -1
  13. data/lib/shamu/entities/list_scope/window_paging.rb +1 -1
  14. data/lib/shamu/entities/null_entity.rb +18 -12
  15. data/lib/shamu/entities/opaque_id.rb +9 -8
  16. data/lib/shamu/error.rb +24 -3
  17. data/lib/shamu/events/active_record/migration.rb +0 -2
  18. data/lib/shamu/events/active_record/service.rb +3 -1
  19. data/lib/shamu/events/events_service.rb +2 -2
  20. data/lib/shamu/events/in_memory/async_service.rb +4 -2
  21. data/lib/shamu/events/in_memory/service.rb +3 -1
  22. data/lib/shamu/features/features_service.rb +3 -1
  23. data/lib/shamu/features/support.rb +1 -1
  24. data/lib/shamu/json_api/rails/controller.rb +1 -1
  25. data/lib/shamu/locale/en.yml +14 -2
  26. data/lib/shamu/security/active_record_policy.rb +2 -1
  27. data/lib/shamu/security/error.rb +1 -1
  28. data/lib/shamu/security/policy.rb +14 -4
  29. data/lib/shamu/security/principal.rb +22 -2
  30. data/lib/shamu/security/roles.rb +4 -3
  31. data/lib/shamu/services.rb +3 -1
  32. data/lib/shamu/services/active_record.rb +2 -1
  33. data/lib/shamu/services/active_record_crud.rb +43 -19
  34. data/lib/shamu/services/lazy_association.rb +50 -18
  35. data/lib/shamu/services/observable_support.rb +73 -0
  36. data/lib/shamu/services/observed_request.rb +76 -0
  37. data/lib/shamu/services/request.rb +12 -0
  38. data/lib/shamu/services/request_support.rb +24 -2
  39. data/lib/shamu/services/result.rb +62 -1
  40. data/lib/shamu/services/service.rb +58 -33
  41. data/lib/shamu/sessions/cookie_store.rb +3 -1
  42. data/lib/shamu/sessions/session_store.rb +2 -2
  43. data/lib/shamu/version.rb +1 -1
  44. data/shamu.gemspec +1 -1
  45. data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +1 -1
  46. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -1
  47. data/spec/lib/shamu/entities/null_entity_spec.rb +6 -1
  48. data/spec/lib/shamu/entities/opaque_id_spec.rb +13 -6
  49. data/spec/lib/shamu/entities/static_repository_spec.rb +2 -2
  50. data/spec/lib/shamu/rails/entity_spec.rb +1 -1
  51. data/spec/lib/shamu/rails/features_spec.rb +1 -1
  52. data/spec/lib/shamu/security/principal_spec.rb +25 -0
  53. data/spec/lib/shamu/services/active_record_crud_spec.rb +39 -4
  54. data/spec/lib/shamu/services/lazy_association_spec.rb +19 -6
  55. data/spec/lib/shamu/services/observable_support_spec.rb +55 -0
  56. data/spec/lib/shamu/services/observed_request_spec.rb +46 -0
  57. data/spec/lib/shamu/services/request_support_spec.rb +54 -3
  58. data/spec/lib/shamu/services/service_spec.rb +16 -27
  59. metadata +15 -5
@@ -9,8 +9,8 @@ module Shamu
9
9
  module OpaqueId
10
10
  module_function
11
11
 
12
- PREFIX = "::".freeze
13
- PATTERN = %r{\A#{ PREFIX }[a-zA-Z0-9+/]+={0,3}\z}
12
+ PATTERN = %r{\A[a-zA-Z0-9+/]+={0,3}\z}
13
+ NUMERICAL = %r{\A[0-9]+\z}
14
14
 
15
15
  # @return [String] an opaque value that uniquely identifies the
16
16
  # entity.
@@ -21,7 +21,7 @@ module Shamu
21
21
  Entity.compose_entity_path( [ entity ] )
22
22
  end
23
23
 
24
- "#{ PREFIX }#{ Base64.strict_encode64( path ) }"
24
+ "#{ Base64.strict_encode64( path ).chomp( '=' ) }"
25
25
  end
26
26
 
27
27
  # @return [String,Integer] the encoded raw record id.
@@ -36,18 +36,19 @@ module Shamu
36
36
 
37
37
  # @return [Array<[String, String]>] decodes the id to it's {EntityPath}.
38
38
  def to_entity_path( opaque_id )
39
- return nil unless opaque_id && opaque_id.start_with?( PREFIX )
39
+ return nil unless opaque_id && NUMERICAL !~ opaque_id
40
40
 
41
- id = opaque_id[ PREFIX.length..-1 ]
42
- id = Base64.strict_decode64( id )
43
- id
41
+ id = opaque_id
42
+ id += "=" * ( 4 - id.length % 4 ) if id.length % 4 > 0
43
+
44
+ Base64.strict_decode64( id )
44
45
  end
45
46
 
46
47
  # @param [String] value candidate value
47
48
  # @return [Boolean] true if the given value is an opaque id.
48
49
  def opaque_id?( value )
49
50
  return unless value
50
- PATTERN =~ value
51
+ PATTERN =~ value && NUMERICAL !~ value
51
52
  end
52
53
  end
53
54
  end
@@ -17,8 +17,29 @@ module Shamu
17
17
 
18
18
  # The resource was not found.
19
19
  class NotFoundError < Error
20
- def initialize( message = :not_found )
21
- super translate( message )
20
+ attr_reader :id
21
+ attr_reader :resource
22
+
23
+ def initialize( message = :not_found, id: :not_set, resource: :not_set )
24
+ @id = id
25
+ @resource = resource
26
+
27
+ if message == :not_found
28
+ message =
29
+ if id != :not_set
30
+ if resource != :not_set
31
+ :resource_not_found_with_id
32
+ else
33
+ :not_found_with_id
34
+ end
35
+ elsif resource != :not_set
36
+ :resource_not_found
37
+ else
38
+ :not_found
39
+ end
40
+ end
41
+
42
+ super translate( message, id: id, resource: resource )
22
43
  end
23
44
  end
24
45
 
@@ -28,4 +49,4 @@ module Shamu
28
49
  super translate( message )
29
50
  end
30
51
  end
31
- end
52
+ end
@@ -7,8 +7,6 @@ module Shamu
7
7
 
8
8
  self.verbose = false
9
9
 
10
- # rubocop:disable Metrics/MethodLength
11
-
12
10
  def up
13
11
  return if data_source_exists? Message.table_name
14
12
 
@@ -27,10 +27,12 @@ module Shamu
27
27
  Migration.new.migrate( :up )
28
28
  end
29
29
 
30
- initialize do
30
+ def initialize
31
31
  self.class.ensure_records!
32
32
  @channels ||= {}
33
33
  @mutex ||= Mutex.new
34
+
35
+ super
34
36
  end
35
37
 
36
38
  # (see EventsService#publish)
@@ -116,7 +116,7 @@ module Shamu
116
116
  def deserialize( raw )
117
117
  hash = MultiJson.load( raw )
118
118
  message_class = hash["class"].constantize
119
- scorpion.fetch message_class, hash["attributes"], {}
119
+ scorpion.fetch message_class, hash["attributes"]
120
120
  end
121
121
 
122
122
  def fetch_channel( name )
@@ -133,4 +133,4 @@ module Shamu
133
133
 
134
134
  end
135
135
  end
136
- end
136
+ end
@@ -9,7 +9,7 @@ module Shamu
9
9
  # to handle events coming in on a separate thread.
10
10
  class AsyncService < InMemory::Service
11
11
 
12
- initialize do
12
+ def initialize
13
13
  ObjectSpace.define_finalizer self do
14
14
  threads = mutex.synchronize do
15
15
  channels.map do |_, state|
@@ -20,6 +20,8 @@ module Shamu
20
20
 
21
21
  ThreadsWait.all_waits( *threads )
22
22
  end
23
+
24
+ super
23
25
  end
24
26
 
25
27
  # (see Service#dispatch)
@@ -45,4 +47,4 @@ module Shamu
45
47
  end
46
48
  end
47
49
  end
48
- end
50
+ end
@@ -12,9 +12,11 @@ module Shamu
12
12
  class Service < EventsService
13
13
  include ChannelStats
14
14
 
15
- initialize do
15
+ def initialize
16
16
  @mutex = Thread::Mutex.new
17
17
  @channels = {}
18
+
19
+ super
18
20
  end
19
21
 
20
22
  # (see EventsService#publish)
@@ -43,8 +43,10 @@ module Shamu
43
43
  # @!method initialize( config_path )
44
44
  # @param
45
45
  # @return [FeaturesService]
46
- initialize do |config_path = nil, **|
46
+ def initialize( config_path = nil )
47
47
  @config_path = config_path || self.class.default_config_path
48
+
49
+ super()
48
50
  end
49
51
 
50
52
  # Indicates if the feature is enabled for the current request/session.
@@ -48,4 +48,4 @@ module Shamu
48
48
  end
49
49
  end
50
50
  end
51
- end
51
+ end
@@ -249,7 +249,7 @@ module Shamu
249
249
  payload
250
250
  end
251
251
 
252
- def map_json_resource_payload( resource ) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
252
+ def map_json_resource_payload( resource ) # rubocop:disable Metrics/AbcSize
253
253
  payload = resource[ :attributes ] ? resource[ :attributes ].dup : {}
254
254
  payload[ :id ] = resource[ :id ] if resource.key?( :id )
255
255
 
@@ -1,8 +1,11 @@
1
1
  en:
2
2
  shamu:
3
3
  errors:
4
- not_found: The resource was not found.
5
- not_implemented: The method has not been implemented.
4
+ not_found: The resource was not found.
5
+ resource_not_found_with_id: The %{resource} with id %{id} was not found.
6
+ not_found_with_id: The resource with id %{id} was not found.
7
+ resource_not_found: The %{resource} was not found.
8
+ not_implemented: The method has not been implemented.
6
9
 
7
10
  warnings:
8
11
 
@@ -19,6 +22,7 @@ en:
19
22
  incomplete_setup: Security has been enabled but is not yet configured.
20
23
  no_actiev_record_policy_checks: Don't check for policy on ActiveRecord resources. Check their projected Entity instead.
21
24
  no_policy_impersonation: Impersonation is not supported by this principal.
25
+
22
26
  events:
23
27
  errors:
24
28
  unknown_runner: Unknown runner. Each process should offer a consitent but unique runner_id.
@@ -32,3 +36,11 @@ en:
32
36
  incomplete_resource: "`identifier` was not called to define the type and id of the resource."
33
37
  no_presenter: No presenter available for %{class} objects. Looked in %{namespaces}.
34
38
  no_json_body: "Missing `data` node for JSON API body. Override `json_request_payload` if no body is expected."
39
+
40
+ activemodel:
41
+ errors:
42
+ models:
43
+ shamu/services/service:
44
+ attributes:
45
+ base:
46
+ canceled: Request was canceled
@@ -25,7 +25,7 @@ module Shamu
25
25
  #
26
26
  # def list
27
27
  # entity_list policy.refine_relation( :list, Model::User.all ) do |record|
28
- # scorpion.fetch UserEntity, { record: record }, {}
28
+ # scorpion.fetch UserEntity, record: record
29
29
  # end
30
30
  # end
31
31
  #
@@ -92,6 +92,7 @@ module Shamu
92
92
  # if no refinement should be applied.
93
93
  # @return [void]
94
94
  def refine( *actions, model_class, &block )
95
+ fail "No actions defined" if actions.blank?
95
96
  refinements << PolicyRefinement.new( expand_aliases( actions ), model_class, block )
96
97
  end
97
98
 
@@ -68,4 +68,4 @@ module Shamu
68
68
  end
69
69
  end
70
70
  end
71
- end
71
+ end
@@ -138,7 +138,9 @@ module Shamu
138
138
  @principal_roles ||= begin
139
139
  expanded = self.class.expand_roles( *roles )
140
140
  expanded << :authenticated if principal.user_id && self.class.role_defined?( :authenticated )
141
- expanded
141
+ expanded.select do |role|
142
+ principal.scoped?( role )
143
+ end
142
144
  end
143
145
  end
144
146
 
@@ -151,6 +153,16 @@ module Shamu
151
153
  principal.try( :user_id ) == id || related_user_ids.include?( id )
152
154
  end
153
155
 
156
+ # @return [Boolean] true if {#principal} has authenticated.
157
+ def authenticated?
158
+ principal.try( :user_id )
159
+ end
160
+
161
+ # @return [Boolean] true if the {#principal} has not authenticated.
162
+ def anonymous?
163
+ !authenticated?
164
+ end
165
+
154
166
  # ============================================================================
155
167
  # @!group DSL
156
168
  #
@@ -179,7 +191,7 @@ module Shamu
179
191
  # @return [void]
180
192
  def permissions
181
193
  if respond_to?( :anonymous_permissions, true ) && respond_to?( :authenticated_permissions, true )
182
- if principal.user_id
194
+ if in_role?( :authenticated )
183
195
  authenticated_permissions
184
196
  else
185
197
  anonymous_permissions
@@ -205,8 +217,6 @@ module Shamu
205
217
  # called if the resource offered to {#permit?} is a Class or Module.
206
218
  #
207
219
  # @example
208
- # end
209
- # end
210
220
  # permit :read, UserEntity
211
221
  # permit :show, :dashboard
212
222
  # permit :update, UserEntity do |user|
@@ -28,14 +28,20 @@ module Shamu
28
28
  attr_reader :elevated
29
29
  alias_method :elevated?, :elevated
30
30
 
31
+ # @!attribute
32
+ # @return [Array<Symbol>] security scopes the principal may be used to
33
+ # authenticate against. When empty, no limits are imposed.
34
+ attr_reader :scopes
35
+
31
36
  #
32
37
  # @!endgroup Attributes
33
38
 
34
- def initialize( user_id: nil, parent_principal: nil, remote_ip: nil, elevated: false )
39
+ def initialize( user_id: nil, parent_principal: nil, remote_ip: nil, elevated: false, scopes: nil )
35
40
  @user_id = user_id
36
41
  @parent_principal = parent_principal
37
42
  @remote_ip = remote_ip
38
43
  @elevated = elevated
44
+ @scopes = scopes
39
45
  end
40
46
 
41
47
  # @return [Array<Object>] all of the user ids in the security principal
@@ -73,6 +79,20 @@ module Shamu
73
79
  def service_delegate?
74
80
  end
75
81
 
82
+ # @param [Symbol] scope
83
+ # @return [Boolean] true if the principal is scoped to authenticate the
84
+ # user for the given scope.
85
+ def scoped?( scope )
86
+ scopes.nil? || scopes.include?( scope )
87
+ end
88
+
89
+ # @!attribute
90
+ # @return [Boolean] true if there is no user associated with the
91
+ # principal.
92
+ def anonymous?
93
+ !user_id
94
+ end
95
+
76
96
  end
77
97
  end
78
- end
98
+ end
@@ -18,9 +18,10 @@ module Shamu
18
18
  # @param [Symbol] name of the role.
19
19
  # @param [Array<Symbol>] inherits additional roles that are
20
20
  # automatically inherited when the named role is granted.
21
+ # @param [Array<Symbol>] scopes that the role may be granted in.
21
22
  # @return [void]
22
- def role( name, inherits: nil )
23
- roles[ name.to_sym ] = { inherits: Array( inherits ) }
23
+ def role( name, inherits: nil, scopes: nil )
24
+ roles[ name.to_sym ] = { inherits: Array( inherits ), scopes: Array( scopes ) }
24
25
  end
25
26
 
26
27
  # Expand the given roles to include the roles that they have inherited.
@@ -38,7 +39,7 @@ module Shamu
38
39
 
39
40
  private
40
41
 
41
- def expand_roles_into( roles, expanded ) # rubocop:disable Metrics/MethodLength
42
+ def expand_roles_into( roles, expanded )
42
43
  raise "No roles defined for #{ name }" unless self.roles.present?
43
44
 
44
45
  roles.each do |name|
@@ -8,5 +8,7 @@ module Shamu
8
8
  require "shamu/services/result"
9
9
  require "shamu/services/lazy_transform"
10
10
  require "shamu/services/lazy_association"
11
+ require "shamu/services/observable_support"
12
+ require "shamu/services/observed_request"
11
13
  end
12
- end
14
+ end
@@ -41,7 +41,8 @@ module Shamu
41
41
 
42
42
  ::ActiveRecord::Base.transaction options do
43
43
  result = yield
44
- raise ::ActiveRecord::Rollback if result && !result.valid?
44
+ success = result && ( result.respond_to?( :valid? ) ? result.valid? : true )
45
+ raise ::ActiveRecord::Rollback unless success
45
46
  end
46
47
 
47
48
  result
@@ -7,7 +7,7 @@ module Shamu
7
7
  # @example
8
8
  #
9
9
  # class UsersService < Shamu::Services::Service
10
- # include Shamu::Services::Crud
10
+ # include Shamu::Services::ActievRecordCrud
11
11
  #
12
12
  # # Define the resource that the service will manage
13
13
  # resource UserEntity, Models::User
@@ -35,7 +35,7 @@ module Shamu
35
35
  # records.pluck( :parent_id )
36
36
  # end
37
37
  #
38
- # scorpion.fetch UserEntity, { parent: parent }, {}
38
+ # scorpion.fetch UserEntity, { parent: parent }
39
39
  # end
40
40
  # end
41
41
  # end
@@ -92,6 +92,10 @@ module Shamu
92
92
  relation
93
93
  end
94
94
 
95
+ def not_found!( id = :not_set )
96
+ raise Shamu::NotFoundError, id: id, resource: entity_class
97
+ end
98
+
95
99
  class_methods do
96
100
 
97
101
  # Declare the entity and resource classes used by the service.
@@ -155,17 +159,18 @@ module Shamu
155
159
  # @yieldparam [Array] args any additional arguments injected by an
156
160
  # overridden {#with_request} method.
157
161
  # @return [void]
158
- def define_create( &block )
159
- define_method :create do |params = nil|
160
- with_request params, request_class( :create ) do |request, *args|
162
+ def define_create( method = :create, &block )
163
+ define_method method do |params = nil|
164
+ with_request params, request_class( method ) do |request, *args|
161
165
  record = request.apply_to( model_class.new )
162
166
 
163
167
  if block_given?
164
168
  result = instance_exec record, request, *args, &block
165
169
  next result if result.is_a?( Services::Result )
170
+ next unless request.valid?
166
171
  end
167
172
 
168
- authorize! :create, build_entity( record ), request
173
+ authorize! method, build_entity( record ), request
169
174
 
170
175
  next record unless record.save
171
176
  build_entity record
@@ -183,12 +188,11 @@ module Shamu
183
188
  # overridden {#with_request} method.
184
189
  # @return [Result] the result of the request.
185
190
  # @return [void]
186
- def define_change( method, default_scope = model_class, &block ) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/LineLength
191
+ def define_change( method, default_scope = model_class, &block )
187
192
  define_method method do |id, params = nil|
188
193
  klass = request_class( method )
189
194
 
190
- params, id = id, id[ :id ] if !params && !id.respond_to?( :to_model_id )
191
- params[ :id ] ||= id if params
195
+ id, params = extract_params( id, params )
192
196
 
193
197
  with_partial_request params, klass do |request, *args|
194
198
  record = default_scope.find( id.to_model_id || request.id )
@@ -204,6 +208,7 @@ module Shamu
204
208
  if block_given?
205
209
  result = instance_exec record, request, *args, &block
206
210
  next result if result.is_a?( Services::Result )
211
+ next unless request.valid?
207
212
  end
208
213
 
209
214
  next record unless record.save
@@ -228,17 +233,21 @@ module Shamu
228
233
  # @param [ActiveRecord::Relation] default_scope to use when finding
229
234
  # records.
230
235
  # @return [void]
231
- def define_destroy( default_scope = model_class, &block )
232
- define_method :destroy do |params|
233
- klass = request_class( :destroy )
236
+ def define_destroy( method = :destroy, default_scope = model_class, &block )
237
+ define_method method do |params|
238
+ klass = request_class( method )
234
239
 
235
240
  params = { id: params } if params.respond_to?( :to_model_id )
236
241
 
237
242
  with_request params, klass do |request, *args|
238
243
  record = default_scope.find( request.id )
239
- authorize! :destroy, build_entity( record ), request
244
+ authorize! method, build_entity( record ), request
245
+
246
+ if block_given?
247
+ instance_exec record, request, *args, &block
248
+ next unless request.valid?
249
+ end
240
250
 
241
- instance_exec record, request, *args, &block if block_given?
242
251
  next record unless record.destroy
243
252
  end
244
253
  end
@@ -269,9 +278,10 @@ module Shamu
269
278
  # @return [void]
270
279
  def define_find( default_scope = model_class.all, &block )
271
280
  if block_given?
281
+ define_method :_find_block, &block
272
282
  define_method :find do |id|
273
283
  wrap_not_found do
274
- record = yield( id )
284
+ record = _find_block( id )
275
285
  authorize! :read, build_entity( record )
276
286
  end
277
287
  end
@@ -295,9 +305,17 @@ module Shamu
295
305
  # underlying resource.
296
306
  # @return [void]
297
307
  def define_lookup( default_scope = model_class.all, &block )
308
+ if block_given?
309
+ define_method :_lookup_block, &block
310
+ else
311
+ define_method :_lookup_block do |ids|
312
+ default_scope.where( id: ids )
313
+ end
314
+ end
315
+
298
316
  define_method :lookup do |*ids|
299
317
  cached_lookup( ids ) do |uncached_ids|
300
- records = block_given? ? yield( uncached_ids ) : default_scope.where( id: uncached_ids )
318
+ records = _lookup_block( uncached_ids )
301
319
  records = authorize_relation :read, records
302
320
  entity_lookup_list records, uncached_ids, entity_class.null_entity
303
321
  end
@@ -319,8 +337,14 @@ module Shamu
319
337
  list_scope = Entities::ListScope.for( entity_class ).coerce( params )
320
338
  authorize! :list, entity_class, list_scope
321
339
 
322
- records = block_given? ? yield( scope ) : scope_relation( default_scope, list_scope )
323
- records = authorize_relation( :read, records, list_scope )
340
+ records =
341
+ if block_given?
342
+ instance_exec( list_scope, &block )
343
+ else
344
+ scope_relation( default_scope, list_scope )
345
+ end
346
+
347
+ records = authorize_relation( :read, records, list_scope )
324
348
 
325
349
  entity_list records
326
350
  end
@@ -346,7 +370,7 @@ module Shamu
346
370
  else
347
371
  define_method :build_entities do |records|
348
372
  records.map do |record|
349
- entity = scorpion.fetch( entity_class, { record: record }, {} )
373
+ entity = scorpion.fetch( entity_class, record: record )
350
374
  authorize! :read, entity
351
375
  end
352
376
  end