simple_authorize 0.1.0 → 1.0.0
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/.simplecov +15 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +1 -1
- data/README.md +210 -15
- data/SECURITY.md +73 -0
- data/lib/generators/simple_authorize/install/install_generator.rb +1 -0
- data/lib/generators/simple_authorize/install/templates/simple_authorize.rb +8 -0
- data/lib/generators/simple_authorize/policy/policy_generator.rb +55 -0
- data/lib/generators/simple_authorize/policy/templates/policy.rb.tt +39 -0
- data/lib/generators/simple_authorize/policy/templates/policy_spec.rb.tt +73 -0
- data/lib/generators/simple_authorize/policy/templates/policy_test.rb.tt +65 -0
- data/lib/simple_authorize/configuration.rb +21 -0
- data/lib/simple_authorize/controller.rb +336 -38
- data/lib/simple_authorize/policy.rb +22 -0
- data/lib/simple_authorize/railtie.rb +20 -0
- data/lib/simple_authorize/rspec.rb +149 -0
- data/lib/simple_authorize/test_helpers.rb +115 -0
- data/lib/simple_authorize/version.rb +1 -1
- data/lib/simple_authorize.rb +6 -17
- data/spec/examples.txt +51 -0
- data/spec/rspec_matchers_spec.rb +235 -0
- data/spec/spec_helper.rb +120 -0
- metadata +62 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleAuthorize
|
|
4
|
+
# Configuration options for SimpleAuthorize
|
|
4
5
|
class Configuration
|
|
5
6
|
# Default error message shown to users when not authorized
|
|
6
7
|
attr_accessor :default_error_message
|
|
@@ -14,11 +15,31 @@ module SimpleAuthorize
|
|
|
14
15
|
# Custom redirect path for unauthorized access
|
|
15
16
|
attr_accessor :unauthorized_redirect_path
|
|
16
17
|
|
|
18
|
+
# Enable policy caching for performance optimization (opt-in)
|
|
19
|
+
attr_accessor :enable_policy_cache
|
|
20
|
+
|
|
21
|
+
# Enable instrumentation for authorization events (default: true)
|
|
22
|
+
attr_accessor :enable_instrumentation
|
|
23
|
+
|
|
24
|
+
# Include detailed error information in API responses (default: false)
|
|
25
|
+
attr_accessor :api_error_details
|
|
26
|
+
|
|
27
|
+
# Enable I18n support for error messages (default: false)
|
|
28
|
+
attr_accessor :i18n_enabled
|
|
29
|
+
|
|
30
|
+
# I18n scope for translations (default: 'simple_authorize')
|
|
31
|
+
attr_accessor :i18n_scope
|
|
32
|
+
|
|
17
33
|
def initialize
|
|
18
34
|
@default_error_message = "You are not authorized to perform this action."
|
|
19
35
|
@auto_verify = false
|
|
20
36
|
@current_user_method = :current_user
|
|
21
37
|
@unauthorized_redirect_path = nil
|
|
38
|
+
@enable_policy_cache = false
|
|
39
|
+
@enable_instrumentation = true
|
|
40
|
+
@api_error_details = false
|
|
41
|
+
@i18n_enabled = false
|
|
42
|
+
@i18n_scope = "simple_authorize"
|
|
22
43
|
end
|
|
23
44
|
end
|
|
24
45
|
|
|
@@ -13,17 +13,73 @@ module SimpleAuthorize
|
|
|
13
13
|
def initialize(options = {})
|
|
14
14
|
if options.is_a?(String)
|
|
15
15
|
# Handle plain string message
|
|
16
|
-
super
|
|
16
|
+
super
|
|
17
17
|
else
|
|
18
18
|
# Handle hash options
|
|
19
19
|
@query = options[:query]
|
|
20
20
|
@record = options[:record]
|
|
21
21
|
@policy = options[:policy]
|
|
22
22
|
|
|
23
|
-
message = options[:message] ||
|
|
23
|
+
message = options[:message] || build_error_message
|
|
24
24
|
super(message)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_error_message
|
|
31
|
+
# Return default message if I18n is disabled
|
|
32
|
+
return "not allowed to #{@query} this #{@record.class}" unless i18n_enabled?
|
|
33
|
+
|
|
34
|
+
# Try to find translation with fallback chain
|
|
35
|
+
translate_error || default_i18n_message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def i18n_enabled?
|
|
39
|
+
SimpleAuthorize.configuration.i18n_enabled
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def translate_error
|
|
43
|
+
return nil unless defined?(I18n)
|
|
44
|
+
return nil unless @policy&.class
|
|
45
|
+
|
|
46
|
+
# Extract action name from query (remove trailing ?)
|
|
47
|
+
action = @query.to_s.delete_suffix("?")
|
|
48
|
+
policy_class_name = @policy.class.name
|
|
49
|
+
return nil unless policy_class_name
|
|
50
|
+
|
|
51
|
+
policy_name = policy_class_name.underscore
|
|
52
|
+
|
|
53
|
+
# Try specific policy + action translation
|
|
54
|
+
key = "#{i18n_scope}.policies.#{policy_name}.#{action}.denied"
|
|
55
|
+
translation = I18n.t(key, **translation_options, default: nil)
|
|
56
|
+
return translation if translation.present?
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
rescue StandardError
|
|
60
|
+
# If any error occurs during translation lookup, return nil to use default
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def translation_options
|
|
65
|
+
{
|
|
66
|
+
record_type: @record&.class&.name || "record",
|
|
67
|
+
record_id: @record.respond_to?(:id) ? @record.id : nil,
|
|
68
|
+
action: @query.to_s.delete_suffix("?"),
|
|
69
|
+
user_role: @policy&.user&.role || "user"
|
|
70
|
+
}.compact
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def default_i18n_message
|
|
74
|
+
return "not allowed to #{@query} this #{@record.class}" unless defined?(I18n)
|
|
75
|
+
|
|
76
|
+
I18n.t("#{i18n_scope}.errors.not_authorized",
|
|
77
|
+
default: "You are not authorized to perform this action")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def i18n_scope
|
|
81
|
+
SimpleAuthorize.configuration.i18n_scope
|
|
82
|
+
end
|
|
27
83
|
end
|
|
28
84
|
|
|
29
85
|
class PolicyNotDefinedError < StandardError; end
|
|
@@ -54,9 +110,15 @@ module SimpleAuthorize
|
|
|
54
110
|
query ||= "#{action_name}?"
|
|
55
111
|
@_policy = policy(record, policy_class: policy_class)
|
|
56
112
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
authorized = @_policy.public_send(query)
|
|
114
|
+
error = nil
|
|
115
|
+
|
|
116
|
+
error = NotAuthorizedError.new(query: query, record: record, policy: @_policy) unless authorized
|
|
117
|
+
|
|
118
|
+
# Emit instrumentation event
|
|
119
|
+
instrument_authorization(record, query, @_policy.class, authorized, error) if instrumentation_enabled?
|
|
120
|
+
|
|
121
|
+
raise error if error
|
|
60
122
|
|
|
61
123
|
@authorization_performed = true
|
|
62
124
|
record
|
|
@@ -70,11 +132,19 @@ module SimpleAuthorize
|
|
|
70
132
|
# Get or instantiate policy for a record
|
|
71
133
|
def policy(record, policy_class: nil, namespace: nil)
|
|
72
134
|
policy_class ||= if namespace
|
|
73
|
-
|
|
135
|
+
policy_class_for(record, namespace: namespace)
|
|
136
|
+
else
|
|
137
|
+
policy_class_for(record)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Return cached policy if caching is enabled
|
|
141
|
+
if SimpleAuthorize.configuration.enable_policy_cache
|
|
142
|
+
policy_cache_key = build_policy_cache_key(record, policy_class)
|
|
143
|
+
@_policy_cache ||= {}
|
|
144
|
+
@_policy_cache[policy_cache_key] ||= policy_class.new(authorized_user, record)
|
|
74
145
|
else
|
|
75
|
-
|
|
146
|
+
policy_class.new(authorized_user, record)
|
|
76
147
|
end
|
|
77
|
-
policy_class.new(authorized_user, record)
|
|
78
148
|
rescue NameError
|
|
79
149
|
raise PolicyNotDefinedError, "unable to find policy `#{policy_class}` for `#{record}`"
|
|
80
150
|
end
|
|
@@ -89,9 +159,21 @@ module SimpleAuthorize
|
|
|
89
159
|
@policy_scoping_performed = true
|
|
90
160
|
|
|
91
161
|
policy_scope_class ||= policy_scope_class_for(scope)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
162
|
+
result = nil
|
|
163
|
+
error = nil
|
|
164
|
+
|
|
165
|
+
begin
|
|
166
|
+
result = policy_scope_class.new(authorized_user, scope).resolve
|
|
167
|
+
rescue NameError
|
|
168
|
+
error = PolicyNotDefinedError.new("unable to find scope `#{policy_scope_class}` for `#{scope}`")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Emit instrumentation event
|
|
172
|
+
instrument_policy_scope(scope, policy_scope_class, error) if instrumentation_enabled?
|
|
173
|
+
|
|
174
|
+
raise error if error
|
|
175
|
+
|
|
176
|
+
result
|
|
95
177
|
end
|
|
96
178
|
|
|
97
179
|
# Ensure scope exists, raising if not found
|
|
@@ -114,6 +196,42 @@ module SimpleAuthorize
|
|
|
114
196
|
end
|
|
115
197
|
end
|
|
116
198
|
|
|
199
|
+
# Get visible attributes for a record
|
|
200
|
+
def visible_attributes(record, action = nil)
|
|
201
|
+
action ||= action_name
|
|
202
|
+
policy = policy(record)
|
|
203
|
+
method_name = "visible_attributes_for_#{action}"
|
|
204
|
+
|
|
205
|
+
if policy.respond_to?(method_name)
|
|
206
|
+
policy.public_send(method_name)
|
|
207
|
+
elsif policy.respond_to?(:visible_attributes)
|
|
208
|
+
policy.visible_attributes
|
|
209
|
+
else
|
|
210
|
+
[]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get editable attributes for a record
|
|
215
|
+
def editable_attributes(record, action = nil)
|
|
216
|
+
action ||= action_name
|
|
217
|
+
policy = policy(record)
|
|
218
|
+
method_name = "editable_attributes_for_#{action}"
|
|
219
|
+
|
|
220
|
+
if policy.respond_to?(method_name)
|
|
221
|
+
policy.public_send(method_name)
|
|
222
|
+
elsif policy.respond_to?(:editable_attributes)
|
|
223
|
+
policy.editable_attributes
|
|
224
|
+
else
|
|
225
|
+
[]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Filter a hash of attributes to only include visible ones
|
|
230
|
+
def filter_attributes(record, attributes)
|
|
231
|
+
visible = visible_attributes(record)
|
|
232
|
+
attributes.select { |key, _value| visible.include?(key.to_sym) }
|
|
233
|
+
end
|
|
234
|
+
|
|
117
235
|
# Automatically build permitted params from policy
|
|
118
236
|
def policy_params(record, param_key = nil)
|
|
119
237
|
param_key ||= record.model_name.param_key
|
|
@@ -123,12 +241,14 @@ module SimpleAuthorize
|
|
|
123
241
|
# Verify that authorization was performed
|
|
124
242
|
def verify_authorized
|
|
125
243
|
return if authorization_performed?
|
|
244
|
+
|
|
126
245
|
raise AuthorizationNotPerformedError, "#{self.class}##{action_name} is missing authorization"
|
|
127
246
|
end
|
|
128
247
|
|
|
129
248
|
# Verify that scoping was performed for index actions
|
|
130
249
|
def verify_policy_scoped
|
|
131
250
|
return if policy_scoped?
|
|
251
|
+
|
|
132
252
|
raise PolicyScopingNotPerformedError, "#{self.class}##{action_name} is missing policy scope"
|
|
133
253
|
end
|
|
134
254
|
|
|
@@ -157,11 +277,17 @@ module SimpleAuthorize
|
|
|
157
277
|
current_user
|
|
158
278
|
end
|
|
159
279
|
|
|
280
|
+
# Clear the policy cache
|
|
281
|
+
def clear_policy_cache
|
|
282
|
+
@_policy_cache = nil
|
|
283
|
+
end
|
|
284
|
+
|
|
160
285
|
# Reset authorization tracking (useful in tests)
|
|
161
286
|
def reset_authorization
|
|
162
287
|
@authorization_performed = nil
|
|
163
288
|
@policy_scoping_performed = nil
|
|
164
289
|
@_policy = nil
|
|
290
|
+
clear_policy_cache
|
|
165
291
|
end
|
|
166
292
|
|
|
167
293
|
# Support for headless policies (policies without a model)
|
|
@@ -169,9 +295,15 @@ module SimpleAuthorize
|
|
|
169
295
|
query ||= "#{action_name}?"
|
|
170
296
|
policy = policy_class.new(authorized_user, nil)
|
|
171
297
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
298
|
+
authorized = policy.public_send(query)
|
|
299
|
+
error = nil
|
|
300
|
+
|
|
301
|
+
error = NotAuthorizedError.new(query: query, record: policy_class, policy: policy) unless authorized
|
|
302
|
+
|
|
303
|
+
# Emit instrumentation event (with nil record for headless policies)
|
|
304
|
+
instrument_authorization(nil, query, policy_class, authorized, error) if instrumentation_enabled?
|
|
305
|
+
|
|
306
|
+
raise error if error
|
|
175
307
|
|
|
176
308
|
@authorization_performed = true
|
|
177
309
|
true
|
|
@@ -180,11 +312,43 @@ module SimpleAuthorize
|
|
|
180
312
|
# Check if user can perform action without raising
|
|
181
313
|
def allowed_to?(action, record, policy_class: nil)
|
|
182
314
|
policy = policy(record, policy_class: policy_class)
|
|
183
|
-
|
|
315
|
+
query = action.to_s.end_with?("?") ? action.to_s : "#{action}?"
|
|
316
|
+
policy.public_send(query)
|
|
184
317
|
rescue PolicyNotDefinedError
|
|
185
318
|
false
|
|
186
319
|
end
|
|
187
320
|
|
|
321
|
+
# Batch Authorization Methods
|
|
322
|
+
|
|
323
|
+
# Authorize all records or raise on first failure
|
|
324
|
+
def authorize_all(records, query = nil, policy_class: nil)
|
|
325
|
+
query ||= "#{action_name}?"
|
|
326
|
+
|
|
327
|
+
records.each do |record|
|
|
328
|
+
authorize(record, query, policy_class: policy_class)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
records
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Return only authorized records
|
|
335
|
+
def authorized_records(records, query = nil, policy_class: nil)
|
|
336
|
+
query ||= "#{action_name}?"
|
|
337
|
+
|
|
338
|
+
records.select do |record|
|
|
339
|
+
allowed_to?(query, record, policy_class: policy_class)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Partition records into [authorized, unauthorized]
|
|
344
|
+
def partition_records(records, query = nil, policy_class: nil)
|
|
345
|
+
query ||= "#{action_name}?"
|
|
346
|
+
|
|
347
|
+
records.partition do |record|
|
|
348
|
+
allowed_to?(query, record, policy_class: policy_class)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
188
352
|
# Get all allowed actions for a record
|
|
189
353
|
def allowed_actions(record)
|
|
190
354
|
policy = policy(record)
|
|
@@ -215,35 +379,165 @@ module SimpleAuthorize
|
|
|
215
379
|
handle_unauthorized(exception)
|
|
216
380
|
end
|
|
217
381
|
|
|
382
|
+
# Check if current request is an API request (JSON/XML)
|
|
383
|
+
def api_request?
|
|
384
|
+
return false unless respond_to?(:request)
|
|
385
|
+
|
|
386
|
+
# Check request format
|
|
387
|
+
return true if request.respond_to?(:format) && (request.format.json? || request.format.xml?)
|
|
388
|
+
|
|
389
|
+
# Check Accept header
|
|
390
|
+
if request.respond_to?(:headers) && request.headers["Accept"]
|
|
391
|
+
accept = request.headers["Accept"].to_s
|
|
392
|
+
return true if accept.include?("application/json") || accept.include?("application/xml")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Check Content-Type header
|
|
396
|
+
if request.respond_to?(:headers) && request.headers["Content-Type"]
|
|
397
|
+
content_type = request.headers["Content-Type"].to_s
|
|
398
|
+
return true if content_type.include?("application/json") || content_type.include?("application/xml")
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
false
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Handle API authorization errors with JSON response
|
|
405
|
+
def handle_api_authorization_error(exception)
|
|
406
|
+
status = exception.record.nil? || authorized_user.nil? ? 401 : 403
|
|
407
|
+
message = SimpleAuthorize.configuration.default_error_message
|
|
408
|
+
|
|
409
|
+
body = {
|
|
410
|
+
error: "not_authorized",
|
|
411
|
+
message: message
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Add detailed information if configured
|
|
415
|
+
if SimpleAuthorize.configuration.api_error_details
|
|
416
|
+
body[:query] = exception.query.to_s
|
|
417
|
+
body[:record_type] = exception.record&.class&.name
|
|
418
|
+
body[:policy] = exception.policy&.class&.name
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
{
|
|
422
|
+
status: status,
|
|
423
|
+
content_type: "application/json",
|
|
424
|
+
body: body
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Build an API error response
|
|
429
|
+
def api_error_response(message:, status: 403, details: nil)
|
|
430
|
+
body = {
|
|
431
|
+
error: "not_authorized",
|
|
432
|
+
message: message
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
body[:details] = details if details
|
|
436
|
+
|
|
437
|
+
{
|
|
438
|
+
status: status,
|
|
439
|
+
content_type: "application/json",
|
|
440
|
+
body: body
|
|
441
|
+
}
|
|
442
|
+
end
|
|
443
|
+
|
|
218
444
|
protected
|
|
219
445
|
|
|
446
|
+
# Build a cache key for a policy instance
|
|
447
|
+
# The key is based on user, record, and policy class to ensure proper scoping
|
|
448
|
+
def build_policy_cache_key(record, policy_class)
|
|
449
|
+
user_key = authorized_user&.id || authorized_user.object_id
|
|
450
|
+
record_key = if record.respond_to?(:id) && record.id.present?
|
|
451
|
+
"#{record.class.name}-#{record.id}"
|
|
452
|
+
else
|
|
453
|
+
"#{record.class.name}-#{record.object_id}"
|
|
454
|
+
end
|
|
455
|
+
policy_key = policy_class.name
|
|
456
|
+
|
|
457
|
+
"#{user_key}/#{record_key}/#{policy_key}"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Check if instrumentation is enabled
|
|
461
|
+
def instrumentation_enabled?
|
|
462
|
+
SimpleAuthorize.configuration.enable_instrumentation
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Emit authorization instrumentation event
|
|
466
|
+
def instrument_authorization(record, query, policy_class, authorized, error)
|
|
467
|
+
ActiveSupport::Notifications.instrument("authorize.simple_authorize",
|
|
468
|
+
build_instrumentation_payload(record, query, policy_class, authorized,
|
|
469
|
+
error))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Emit policy scope instrumentation event
|
|
473
|
+
def instrument_policy_scope(scope, policy_scope_class, error)
|
|
474
|
+
ActiveSupport::Notifications.instrument("policy_scope.simple_authorize",
|
|
475
|
+
build_scope_payload(scope, policy_scope_class, error))
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Build payload for authorization events
|
|
479
|
+
def build_instrumentation_payload(record, query, policy_class, authorized, error)
|
|
480
|
+
payload = {
|
|
481
|
+
user: authorized_user,
|
|
482
|
+
user_id: authorized_user&.id,
|
|
483
|
+
record: record,
|
|
484
|
+
record_id: record.respond_to?(:id) ? record&.id : nil,
|
|
485
|
+
record_class: record&.class&.name,
|
|
486
|
+
query: query.to_s,
|
|
487
|
+
policy_class: policy_class,
|
|
488
|
+
authorized: authorized,
|
|
489
|
+
error: error
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Add controller and action info if available
|
|
493
|
+
payload[:controller] = controller_name if respond_to?(:controller_name)
|
|
494
|
+
payload[:action] = action_name if respond_to?(:action_name)
|
|
495
|
+
|
|
496
|
+
payload
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Build payload for policy scope events
|
|
500
|
+
def build_scope_payload(scope, policy_scope_class, error)
|
|
501
|
+
payload = {
|
|
502
|
+
user: authorized_user,
|
|
503
|
+
user_id: authorized_user&.id,
|
|
504
|
+
scope: scope,
|
|
505
|
+
policy_scope_class: policy_scope_class,
|
|
506
|
+
error: error
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# Add controller and action info if available
|
|
510
|
+
payload[:controller] = controller_name if respond_to?(:controller_name)
|
|
511
|
+
payload[:action] = action_name if respond_to?(:action_name)
|
|
512
|
+
|
|
513
|
+
payload
|
|
514
|
+
end
|
|
515
|
+
|
|
220
516
|
def policy_class_for(record, namespace: nil)
|
|
221
517
|
klass = record.class
|
|
222
518
|
record_class = if record.is_a?(Class)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
519
|
+
record.name
|
|
520
|
+
elsif record.respond_to?(:model_name)
|
|
521
|
+
record.model_name.to_s
|
|
522
|
+
elsif klass.respond_to?(:model_name)
|
|
523
|
+
klass.model_name.to_s
|
|
524
|
+
else
|
|
525
|
+
klass.name
|
|
526
|
+
end
|
|
231
527
|
|
|
232
528
|
policy_class_name = if namespace
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
529
|
+
"#{namespace.to_s.camelize}::#{record_class}Policy"
|
|
530
|
+
else
|
|
531
|
+
"#{record_class}Policy"
|
|
532
|
+
end
|
|
237
533
|
|
|
238
534
|
begin
|
|
239
535
|
policy_class_name.constantize
|
|
240
536
|
rescue NameError
|
|
241
537
|
# Fall back to non-namespaced policy if namespaced one doesn't exist
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
raise
|
|
246
|
-
end
|
|
538
|
+
raise unless namespace
|
|
539
|
+
|
|
540
|
+
"#{record_class}Policy".constantize
|
|
247
541
|
end
|
|
248
542
|
end
|
|
249
543
|
|
|
@@ -259,7 +553,15 @@ module SimpleAuthorize
|
|
|
259
553
|
|
|
260
554
|
# Handle authorization errors
|
|
261
555
|
def handle_unauthorized(exception = nil)
|
|
262
|
-
|
|
556
|
+
# Handle API requests with JSON response
|
|
557
|
+
if api_request? && exception.is_a?(NotAuthorizedError)
|
|
558
|
+
response = handle_api_authorization_error(exception)
|
|
559
|
+
render json: response[:body], status: response[:status]
|
|
560
|
+
return
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Handle traditional HTML requests with redirect
|
|
564
|
+
flash[:alert] = SimpleAuthorize.configuration.default_error_message
|
|
263
565
|
safe_redirect_path = safe_referrer_path || root_path
|
|
264
566
|
|
|
265
567
|
if exception
|
|
@@ -278,11 +580,7 @@ module SimpleAuthorize
|
|
|
278
580
|
request_uri = URI.parse(request.url)
|
|
279
581
|
|
|
280
582
|
# Only allow referrers from the same host
|
|
281
|
-
if referrer_uri.host == request_uri.host
|
|
282
|
-
referrer_uri.path
|
|
283
|
-
else
|
|
284
|
-
nil
|
|
285
|
-
end
|
|
583
|
+
referrer_uri.path if referrer_uri.host == request_uri.host
|
|
286
584
|
rescue URI::InvalidURIError
|
|
287
585
|
nil
|
|
288
586
|
end
|
|
@@ -39,6 +39,28 @@ module SimpleAuthorize
|
|
|
39
39
|
false
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Attribute-level authorization
|
|
43
|
+
|
|
44
|
+
# Returns array of attributes visible to the user
|
|
45
|
+
def visible_attributes
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns array of attributes editable by the user
|
|
50
|
+
def editable_attributes
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if a specific attribute is visible
|
|
55
|
+
def attribute_visible?(attribute)
|
|
56
|
+
visible_attributes.include?(attribute.to_sym)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if a specific attribute is editable
|
|
60
|
+
def attribute_editable?(attribute)
|
|
61
|
+
editable_attributes.include?(attribute.to_sym)
|
|
62
|
+
end
|
|
63
|
+
|
|
42
64
|
# Helper methods
|
|
43
65
|
protected
|
|
44
66
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleAuthorize
|
|
4
|
+
# Railtie for automatic integration with Rails
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
initializer "simple_authorize.configure" do
|
|
7
|
+
ActiveSupport.on_load(:action_controller) do
|
|
8
|
+
# Make SimpleAuthorize::Controller available as Authorization for backwards compatibility
|
|
9
|
+
::Authorization = SimpleAuthorize::Controller unless defined?(::Authorization)
|
|
10
|
+
# Make SimpleAuthorize::Policy available as ApplicationPolicy for backwards compatibility
|
|
11
|
+
::ApplicationPolicy = SimpleAuthorize::Policy unless defined?(::ApplicationPolicy)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate initializer template
|
|
16
|
+
generators do
|
|
17
|
+
require_relative "../generators/simple_authorize/install/install_generator"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|