action_policy 0.4.4 → 0.5.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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +203 -174
  3. data/README.md +5 -4
  4. data/lib/action_policy.rb +7 -1
  5. data/lib/action_policy/behaviour.rb +22 -16
  6. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  7. data/lib/action_policy/behaviours/scoping.rb +2 -1
  8. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  9. data/lib/action_policy/ext/module_namespace.rb +1 -6
  10. data/lib/action_policy/ext/policy_cache_key.rb +10 -30
  11. data/lib/action_policy/i18n.rb +1 -1
  12. data/lib/action_policy/lookup_chain.rb +29 -15
  13. data/lib/action_policy/policy/aliases.rb +7 -12
  14. data/lib/action_policy/policy/authorization.rb +8 -7
  15. data/lib/action_policy/policy/cache.rb +11 -17
  16. data/lib/action_policy/policy/core.rb +25 -12
  17. data/lib/action_policy/policy/defaults.rb +3 -9
  18. data/lib/action_policy/policy/execution_result.rb +3 -9
  19. data/lib/action_policy/policy/pre_check.rb +19 -58
  20. data/lib/action_policy/policy/reasons.rb +29 -19
  21. data/lib/action_policy/policy/scoping.rb +5 -6
  22. data/lib/action_policy/rails/controller.rb +6 -1
  23. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  24. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  25. data/lib/action_policy/rspec/dsl.rb +1 -1
  26. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  27. data/lib/action_policy/utils/pretty_print.rb +21 -24
  28. data/lib/action_policy/utils/suggest_message.rb +1 -3
  29. data/lib/action_policy/version.rb +1 -1
  30. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  31. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  32. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  33. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  34. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  35. metadata +29 -119
  36. data/.gitattributes +0 -2
  37. data/.github/ISSUE_TEMPLATE.md +0 -21
  38. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  39. data/.github/bug_report_template.rb +0 -175
  40. data/.gitignore +0 -15
  41. data/.rubocop.yml +0 -54
  42. data/.tidelift.yml +0 -6
  43. data/.travis.yml +0 -31
  44. data/Gemfile +0 -22
  45. data/Rakefile +0 -27
  46. data/action_policy.gemspec +0 -44
  47. data/benchmarks/namespaced_lookup_cache.rb +0 -74
  48. data/benchmarks/pre_checks.rb +0 -73
  49. data/bin/console +0 -14
  50. data/bin/setup +0 -8
  51. data/docs/.nojekyll +0 -0
  52. data/docs/CNAME +0 -1
  53. data/docs/README.md +0 -79
  54. data/docs/_sidebar.md +0 -27
  55. data/docs/aliases.md +0 -122
  56. data/docs/assets/docsify-search.js +0 -364
  57. data/docs/assets/docsify.min.js +0 -3
  58. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  59. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  60. data/docs/assets/images/banner.png +0 -0
  61. data/docs/assets/images/cache.png +0 -0
  62. data/docs/assets/images/cache.svg +0 -70
  63. data/docs/assets/images/layer.png +0 -0
  64. data/docs/assets/images/layer.svg +0 -35
  65. data/docs/assets/prism-ruby.min.js +0 -1
  66. data/docs/assets/styles.css +0 -347
  67. data/docs/assets/vue.min.css +0 -1
  68. data/docs/authorization_context.md +0 -92
  69. data/docs/behaviour.md +0 -113
  70. data/docs/caching.md +0 -291
  71. data/docs/controller_action_aliases.md +0 -109
  72. data/docs/custom_lookup_chain.md +0 -48
  73. data/docs/custom_policy.md +0 -53
  74. data/docs/debugging.md +0 -55
  75. data/docs/decorators.md +0 -27
  76. data/docs/favicon.ico +0 -0
  77. data/docs/graphql.md +0 -302
  78. data/docs/i18n.md +0 -44
  79. data/docs/index.html +0 -43
  80. data/docs/instrumentation.md +0 -84
  81. data/docs/lookup_chain.md +0 -22
  82. data/docs/namespaces.md +0 -77
  83. data/docs/non_rails.md +0 -28
  84. data/docs/pre_checks.md +0 -57
  85. data/docs/pundit_migration.md +0 -80
  86. data/docs/quick_start.md +0 -118
  87. data/docs/rails.md +0 -120
  88. data/docs/reasons.md +0 -120
  89. data/docs/scoping.md +0 -255
  90. data/docs/testing.md +0 -390
  91. data/docs/writing_policies.md +0 -107
  92. data/gemfiles/jruby.gemfile +0 -8
  93. data/gemfiles/rails42.gemfile +0 -9
  94. data/gemfiles/rails6.gemfile +0 -8
  95. data/gemfiles/railsmaster.gemfile +0 -6
  96. data/lib/action_policy/ext/string_match.rb +0 -14
  97. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -11,6 +11,8 @@ unless "".respond_to?(:underscore)
11
11
  end
12
12
 
13
13
  module ActionPolicy
14
+ using RubyNext
15
+
14
16
  # Raised when `resolve_rule` failed to find an approriate
15
17
  # policy rule method for the activity
16
18
  class UnknownRule < Error
@@ -75,15 +77,28 @@ module ActionPolicy
75
77
  # `apply` also calls pre-checks.
76
78
  def apply(rule)
77
79
  @result = self.class.result_class.new(self.class, rule)
78
- @result.load __apply__(rule)
80
+
81
+ catch :policy_fulfilled do
82
+ result.load __apply__(rule)
83
+ end
84
+
85
+ result.value
86
+ end
87
+
88
+ def deny!
89
+ result&.load false
90
+ throw :policy_fulfilled
91
+ end
92
+
93
+ def allow!
94
+ result&.load true
95
+ throw :policy_fulfilled
79
96
  end
80
97
 
81
98
  # This method performs the rule call.
82
99
  # Override or extend it to provide custom functionality
83
100
  # (such as caching, pre checks, etc.)
84
- def __apply__(rule)
85
- public_send(rule)
86
- end
101
+ def __apply__(rule) = public_send(rule)
87
102
 
88
103
  # Wrap code that could modify result
89
104
  # to prevent the current result modification
@@ -102,16 +117,16 @@ module ActionPolicy
102
117
  # If record is `nil` then we uses the current policy.
103
118
  def allowed_to?(rule, record = :__undef__, **options)
104
119
  if (record == :__undef__ || record == self.record) && options.empty?
105
- __apply__(rule)
120
+ __apply__(resolve_rule(rule))
106
121
  else
107
- policy_for(record: record, **options).apply(rule)
122
+ policy_for(record: record, **options).then do |policy|
123
+ policy.apply(policy.resolve_rule(rule))
124
+ end
108
125
  end
109
126
  end
110
127
 
111
128
  # An alias for readability purposes
112
- def check?(*args)
113
- allowed_to?(*args)
114
- end
129
+ def check?(*args) = allowed_to?(*args)
115
130
 
116
131
  # Returns a rule name (policy method name) for activity.
117
132
  #
@@ -127,9 +142,7 @@ module ActionPolicy
127
142
  # Return annotated source code for the rule
128
143
  # NOTE: require "method_source" and "unparser" gems to be installed.
129
144
  # Otherwise returns empty string.
130
- def inspect_rule(rule)
131
- PrettyPrint.print_method(self, rule)
132
- end
145
+ def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
133
146
 
134
147
  # Helper for printing the annotated rule source.
135
148
  # Useful for debugging: type `pp :show?` within the context of the policy
@@ -21,17 +21,11 @@ module ActionPolicy
21
21
  base.authorize :user
22
22
  end
23
23
 
24
- def index?
25
- false
26
- end
24
+ def index?() = false
27
25
 
28
- def create?
29
- false
30
- end
26
+ def create?() = false
31
27
 
32
- def manage?
33
- false
34
- end
28
+ def manage?() = false
35
29
  end
36
30
  end
37
31
  end
@@ -19,21 +19,15 @@ module ActionPolicy
19
19
  @value = value
20
20
  end
21
21
 
22
- def success?
23
- @value == true
24
- end
22
+ def success?() = @value == true
25
23
 
26
- def fail?
27
- @value == false
28
- end
24
+ def fail?() = @value == false
29
25
 
30
26
  def cached!
31
27
  @cached = true
32
28
  end
33
29
 
34
- def cached?
35
- @cached == true
36
- end
30
+ def cached?() = @cached == true
37
31
 
38
32
  def inspect
39
33
  "<#{policy}##{rule}: #{@value}>"
@@ -31,40 +31,12 @@ module ActionPolicy
31
31
  #
32
32
  # Implements filtering logic.
33
33
  class Check
34
- # Wrapper over check result
35
- class Result
36
- DENY = :__deny__
37
- ALLOW = :__allow__
38
-
39
- KINDS = {
40
- DENY => false,
41
- ALLOW => true
42
- }.freeze
43
-
44
- attr_reader :fulfilled
45
-
46
- def initialize(val)
47
- @fulfilled = KINDS.key?(val)
48
- @value = val
49
- end
50
-
51
- alias fulfilled? fulfilled
52
-
53
- def denied?
54
- @value == DENY
55
- end
56
-
57
- def value
58
- KINDS.fetch(@value)
59
- end
60
- end
61
-
62
34
  attr_reader :name, :policy_class
63
35
 
64
36
  def initialize(policy, name, except: nil, only: nil)
65
37
  if !except.nil? && !only.nil?
66
38
  raise ArgumentError,
67
- "Only one of `except` and `only` may be specified for pre-check"
39
+ "Only one of `except` and `only` may be specified for pre-check"
68
40
  end
69
41
 
70
42
  @policy_class = policy
@@ -80,23 +52,17 @@ module ActionPolicy
80
52
  filter.call(rule)
81
53
  end
82
54
 
83
- def call(policy)
84
- Result.new(policy.send(name)).tap do |res|
85
- # add denial reason if Reasons included
86
- policy.result.reasons.add(policy, name) if
87
- res.denied? && policy.result.respond_to?(:reasons)
88
- end
89
- end
55
+ def call(policy) = policy.send(name)
90
56
 
91
57
  def skip!(except: nil, only: nil)
92
58
  if !except.nil? && !only.nil?
93
59
  raise ArgumentError,
94
- "Only one of `except` and `only` may be specified when skipping pre-check"
60
+ "Only one of `except` and `only` may be specified when skipping pre-check"
95
61
  end
96
62
 
97
63
  if except.nil? && only.nil?
98
64
  raise ArgumentError,
99
- "At least one of `except` and `only` must be specified when skipping pre-check"
65
+ "At least one of `except` and `only` must be specified when skipping pre-check"
100
66
  end
101
67
 
102
68
  if except
@@ -116,7 +82,12 @@ module ActionPolicy
116
82
  # rubocop: enable
117
83
 
118
84
  def dup
119
- self.class.new(policy_class, name, except: blacklist&.dup, only: whitelist&.dup)
85
+ self.class.new(
86
+ policy_class,
87
+ name,
88
+ except: blacklist&.dup,
89
+ only: whitelist&.dup
90
+ )
120
91
  end
121
92
 
122
93
  private
@@ -142,21 +113,12 @@ module ActionPolicy
142
113
  def run_pre_checks(rule)
143
114
  self.class.pre_checks.each do |check|
144
115
  next unless check.applicable?(rule)
145
- res = check.call(self)
146
- return res.value if res.fulfilled?
116
+ check.call(self)
147
117
  end
148
118
 
149
119
  yield if block_given?
150
120
  end
151
121
 
152
- def deny!
153
- Check::Result::DENY
154
- end
155
-
156
- def allow!
157
- Check::Result::ALLOW
158
- end
159
-
160
122
  def __apply__(rule)
161
123
  run_pre_checks(rule) { super }
162
124
  end
@@ -165,7 +127,7 @@ module ActionPolicy
165
127
  def pre_check(*names, **options)
166
128
  names.each do |name|
167
129
  # do not allow pre-check override
168
- check = pre_checks.find { |c| c.name == name }
130
+ check = pre_checks.find { _1.name == name }
169
131
  raise "Pre-check already defined: #{name}" unless check.nil?
170
132
 
171
133
  pre_checks << Check.new(self, name, **options)
@@ -174,26 +136,25 @@ module ActionPolicy
174
136
 
175
137
  def skip_pre_check(*names, **options)
176
138
  names.each do |name|
177
- check = pre_checks.find { |c| c.name == name }
139
+ check = pre_checks.find { _1.name == name }
178
140
  raise "Pre-check not found: #{name}" if check.nil?
179
141
 
180
142
  # when no options provided we remove this check completely
181
143
  next pre_checks.delete(check) if options.empty?
182
144
 
183
145
  # otherwise duplicate and apply skip options
184
- pre_checks[pre_checks.index(check)] = check.dup.tap { |c| c.skip!(**options) }
146
+ pre_checks[pre_checks.index(check)] = check.dup.tap { _1.skip!(**options) }
185
147
  end
186
148
  end
187
149
 
188
150
  def pre_checks
189
151
  return @pre_checks if instance_variable_defined?(:@pre_checks)
190
152
 
191
- @pre_checks =
192
- if superclass.respond_to?(:pre_checks)
193
- superclass.pre_checks.dup
194
- else
195
- []
196
- end
153
+ if superclass.respond_to?(:pre_checks)
154
+ superclass.pre_checks.dup
155
+ else
156
+ []
157
+ end => @pre_checks
197
158
  end
198
159
  end
199
160
  end
@@ -1,16 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless "".respond_to?(:then)
4
- require "action_policy/ext/yield_self_then"
5
- using ActionPolicy::Ext::YieldSelfThen
6
- end
7
-
8
- unless {}.respond_to?(:transform_keys)
9
- require "action_policy/ext/hash_transform_keys"
10
- using ActionPolicy::Ext::HashTransformKeys
11
- end
12
-
13
3
  module ActionPolicy
4
+ using RubyNext
5
+
14
6
  module Policy
15
7
  # Failures reasons store
16
8
  class FailureReasons
@@ -33,17 +25,11 @@ module ActionPolicy
33
25
 
34
26
  # Return Hash of the form:
35
27
  # { policy_identifier => [rules, ...] }
36
- def details
37
- reasons.transform_keys(&:identifier)
38
- end
28
+ def details() = reasons.transform_keys(&:identifier)
39
29
 
40
- def empty?
41
- reasons.empty?
42
- end
30
+ def empty?() = reasons.empty?
43
31
 
44
- def present?
45
- !empty?
46
- end
32
+ def present?() = !empty?
47
33
 
48
34
  private
49
35
 
@@ -82,6 +68,25 @@ module ActionPolicy
82
68
  @details = nil
83
69
  end
84
70
 
71
+ # Returns all the details merged together
72
+ def all_details
73
+ return @all_details if defined?(@all_details)
74
+
75
+ {}.tap do |all|
76
+ next unless defined?(@reasons)
77
+
78
+ reasons.reasons.each_value do |rules|
79
+ detailed_reasons = rules.last
80
+
81
+ next unless detailed_reasons.is_a?(Hash)
82
+
83
+ detailed_reasons.each_value do |details|
84
+ all.merge!(details)
85
+ end
86
+ end
87
+ end => @all_details
88
+ end
89
+
85
90
  # Add reasons to inspect
86
91
  def inspect
87
92
  super.then do |str|
@@ -195,6 +200,11 @@ module ActionPolicy
195
200
 
196
201
  res.success?
197
202
  end
203
+
204
+ def deny!(reason = nil)
205
+ result&.reasons&.add(self, reason) if reason
206
+ super()
207
+ end
198
208
  end
199
209
  end
200
210
  end
@@ -148,12 +148,11 @@ module ActionPolicy
148
148
  def scope_matchers
149
149
  return @scope_matchers if instance_variable_defined?(:@scope_matchers)
150
150
 
151
- @scope_matchers =
152
- if superclass.respond_to?(:scope_matchers)
153
- superclass.scope_matchers.dup
154
- else
155
- []
156
- end
151
+ if superclass.respond_to?(:scope_matchers)
152
+ superclass.scope_matchers.dup
153
+ else
154
+ []
155
+ end => @scope_matchers
157
156
  end
158
157
  end
159
158
  end
@@ -26,6 +26,7 @@ module ActionPolicy
26
26
  helper_method :allowed_to? if respond_to?(:helper_method)
27
27
 
28
28
  attr_writer :authorize_count
29
+ attr_reader :verify_authorized_skipped
29
30
 
30
31
  protected :authorize_count=, :authorize_count
31
32
  end
@@ -57,13 +58,17 @@ module ActionPolicy
57
58
 
58
59
  def verify_authorized
59
60
  raise UnauthorizedAction.new(controller_path, action_name) if
60
- authorize_count.zero?
61
+ authorize_count.zero? && !verify_authorized_skipped
61
62
  end
62
63
 
63
64
  def authorize_count
64
65
  @authorize_count ||= 0
65
66
  end
66
67
 
68
+ def skip_verify_authorized!
69
+ @verify_authorized_skipped = true
70
+ end
71
+
67
72
  class_methods do
68
73
  # Adds after_action callback to check that
69
74
  # authorize! method has been called.
@@ -11,7 +11,7 @@ module ActionPolicy # :nodoc:
11
11
  INIT_EVENT_NAME = "action_policy.init"
12
12
  APPLY_EVENT_NAME = "action_policy.apply_rule"
13
13
 
14
- def initialize(*)
14
+ def initialize(record = nil, **params)
15
15
  event = {policy: self.class.name}
16
16
  ActiveSupport::Notifications.instrument(INIT_EVENT_NAME, event) { super }
17
17
  end
@@ -45,16 +45,14 @@ module ActionPolicy
45
45
 
46
46
  @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
47
47
 
48
- actual_calls.any? { |call| call.matches?(policy, rule, target) }
48
+ actual_calls.any? { _1.matches?(policy, rule, target) }
49
49
  end
50
50
 
51
51
  def does_not_match?(*)
52
52
  raise "This matcher doesn't support negation"
53
53
  end
54
54
 
55
- def supports_block_expectations?
56
- true
57
- end
55
+ def supports_block_expectations?() = true
58
56
 
59
57
  def failure_message
60
58
  "expected #{formatted_record} " \
@@ -72,14 +70,12 @@ module ActionPolicy
72
70
  end
73
71
 
74
72
  def formatted_calls
75
- actual_calls.map do |acall|
76
- " - #{acall.inspect}"
73
+ actual_calls.map do
74
+ " - #{_1.inspect}"
77
75
  end.join("\n")
78
76
  end
79
77
 
80
- def formatted_record(record = target)
81
- ::RSpec::Support::ObjectFormatter.format(record)
82
- end
78
+ def formatted_record(record = target) = ::RSpec::Support::ObjectFormatter.format(record)
83
79
  end
84
80
  end
85
81
  end
@@ -50,7 +50,7 @@ if defined?(::RSpec)
50
50
  ::RSpec.shared_context "action_policy:policy_context" do
51
51
  let(:record) { nil }
52
52
  let(:context) { {} }
53
- let(:policy) { described_class.new(record, context) }
53
+ let(:policy) { described_class.new(record, **context) }
54
54
  end
55
55
 
56
56
  ::RSpec.shared_context "action_policy:policy_rule_context" do |policy_rule, *args, method: "describe", block: nil|
@@ -21,7 +21,7 @@ module ActionPolicy
21
21
  #
22
22
  class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
23
  attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations
24
+ :target_expectations
25
25
 
26
26
  def initialize(type)
27
27
  @type = type
@@ -56,7 +56,7 @@ module ActionPolicy
56
56
 
57
57
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
58
 
59
- matching_scopes = actual_scopes.select { |scope| scope.matches?(policy, type, name, scope_options) }
59
+ matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
60
60
 
61
61
  return false if matching_scopes.empty?
62
62
 
@@ -75,9 +75,7 @@ module ActionPolicy
75
75
  raise "This matcher doesn't support negation"
76
76
  end
77
77
 
78
- def supports_block_expectations?
79
- true
80
- end
78
+ def supports_block_expectations?() = true
81
79
 
82
80
  def failure_message
83
81
  "expected a scoping named :#{name} for type :#{type} " \
@@ -109,8 +107,8 @@ module ActionPolicy
109
107
  end
110
108
 
111
109
  def formatted_scopings
112
- actual_scopes.map do |ascope|
113
- " - #{ascope.inspect}"
110
+ actual_scopes.map do
111
+ " - #{_1.inspect}"
114
112
  end.join("\n")
115
113
  end
116
114
  end