shamu 0.0.21 → 0.0.24
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/Gemfile +3 -1
- data/Gemfile.lock +70 -69
- data/app/README +1 -0
- data/config/environment.rb +1 -0
- data/lib/shamu/attributes.rb +26 -0
- data/lib/shamu/attributes/equality.rb +10 -1
- data/lib/shamu/entities/entity.rb +11 -0
- data/lib/shamu/entities/list_scope.rb +3 -0
- data/lib/shamu/entities/list_scope/sorting.rb +6 -1
- data/lib/shamu/entities/list_scope/window_paging.rb +1 -1
- data/lib/shamu/entities/null_entity.rb +18 -12
- data/lib/shamu/entities/opaque_id.rb +9 -8
- data/lib/shamu/error.rb +24 -3
- data/lib/shamu/events/active_record/migration.rb +0 -2
- data/lib/shamu/events/active_record/service.rb +3 -1
- data/lib/shamu/events/events_service.rb +2 -2
- data/lib/shamu/events/in_memory/async_service.rb +4 -2
- data/lib/shamu/events/in_memory/service.rb +3 -1
- data/lib/shamu/features/features_service.rb +3 -1
- data/lib/shamu/features/support.rb +1 -1
- data/lib/shamu/json_api/rails/controller.rb +1 -1
- data/lib/shamu/locale/en.yml +14 -2
- data/lib/shamu/security/active_record_policy.rb +2 -1
- data/lib/shamu/security/error.rb +1 -1
- data/lib/shamu/security/policy.rb +14 -4
- data/lib/shamu/security/principal.rb +22 -2
- data/lib/shamu/security/roles.rb +4 -3
- data/lib/shamu/services.rb +3 -1
- data/lib/shamu/services/active_record.rb +2 -1
- data/lib/shamu/services/active_record_crud.rb +43 -19
- data/lib/shamu/services/lazy_association.rb +50 -18
- data/lib/shamu/services/observable_support.rb +73 -0
- data/lib/shamu/services/observed_request.rb +76 -0
- data/lib/shamu/services/request.rb +12 -0
- data/lib/shamu/services/request_support.rb +24 -2
- data/lib/shamu/services/result.rb +62 -1
- data/lib/shamu/services/service.rb +58 -33
- data/lib/shamu/sessions/cookie_store.rb +3 -1
- data/lib/shamu/sessions/session_store.rb +2 -2
- data/lib/shamu/version.rb +1 -1
- data/shamu.gemspec +1 -1
- data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +1 -1
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -1
- data/spec/lib/shamu/entities/null_entity_spec.rb +6 -1
- data/spec/lib/shamu/entities/opaque_id_spec.rb +13 -6
- data/spec/lib/shamu/entities/static_repository_spec.rb +2 -2
- data/spec/lib/shamu/rails/entity_spec.rb +1 -1
- data/spec/lib/shamu/rails/features_spec.rb +1 -1
- data/spec/lib/shamu/security/principal_spec.rb +25 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +39 -4
- data/spec/lib/shamu/services/lazy_association_spec.rb +19 -6
- data/spec/lib/shamu/services/observable_support_spec.rb +55 -0
- data/spec/lib/shamu/services/observed_request_spec.rb +46 -0
- data/spec/lib/shamu/services/request_support_spec.rb +54 -3
- data/spec/lib/shamu/services/service_spec.rb +16 -27
- metadata +15 -5
@@ -9,8 +9,8 @@ module Shamu
|
|
9
9
|
module OpaqueId
|
10
10
|
module_function
|
11
11
|
|
12
|
-
|
13
|
-
|
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
|
-
"#{
|
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 &&
|
39
|
+
return nil unless opaque_id && NUMERICAL !~ opaque_id
|
40
40
|
|
41
|
-
id = opaque_id
|
42
|
-
id =
|
43
|
-
|
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
|
data/lib/shamu/error.rb
CHANGED
@@ -17,8 +17,29 @@ module Shamu
|
|
17
17
|
|
18
18
|
# The resource was not found.
|
19
19
|
class NotFoundError < Error
|
20
|
-
|
21
|
-
|
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
|
@@ -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
|
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
|
@@ -43,8 +43,10 @@ module Shamu
|
|
43
43
|
# @!method initialize( config_path )
|
44
44
|
# @param
|
45
45
|
# @return [FeaturesService]
|
46
|
-
initialize
|
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.
|
@@ -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
|
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
|
|
data/lib/shamu/locale/en.yml
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
en:
|
2
2
|
shamu:
|
3
3
|
errors:
|
4
|
-
|
5
|
-
|
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,
|
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
|
|
data/lib/shamu/security/error.rb
CHANGED
@@ -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
|
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
|
data/lib/shamu/security/roles.rb
CHANGED
@@ -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 )
|
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|
|
data/lib/shamu/services.rb
CHANGED
@@ -41,7 +41,8 @@ module Shamu
|
|
41
41
|
|
42
42
|
::ActiveRecord::Base.transaction options do
|
43
43
|
result = yield
|
44
|
-
|
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::
|
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
|
160
|
-
with_request params, request_class(
|
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!
|
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 )
|
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
|
-
|
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
|
233
|
-
klass = request_class(
|
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!
|
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 =
|
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 =
|
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
|
323
|
-
|
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,
|
373
|
+
entity = scorpion.fetch( entity_class, record: record )
|
350
374
|
authorize! :read, entity
|
351
375
|
end
|
352
376
|
end
|