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