shamu 0.0.21 → 0.0.24

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 (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