action_policy 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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