action_policy 0.5.2 → 0.5.7

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -2
  3. data/LICENSE.txt +1 -1
  4. data/README.md +1 -1
  5. data/config/rubocop-rspec.yml +17 -0
  6. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/behaviours/policy_for.rb +1 -1
  7. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/behaviours/scoping.rb +2 -2
  8. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/policy/authorization.rb +0 -0
  9. data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +159 -0
  10. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +1 -1
  11. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +2 -2
  12. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +1 -1
  13. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +8 -0
  14. data/lib/.rbnext/3.0/action_policy/policy/core.rb +9 -2
  15. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +2 -0
  16. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +2 -2
  17. data/lib/action_policy.rb +2 -0
  18. data/lib/action_policy/behaviour.rb +2 -2
  19. data/lib/action_policy/behaviours/memoized.rb +1 -1
  20. data/lib/action_policy/behaviours/namespaced.rb +1 -1
  21. data/lib/action_policy/behaviours/policy_for.rb +1 -1
  22. data/lib/action_policy/behaviours/scoping.rb +2 -2
  23. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  24. data/lib/action_policy/lookup_chain.rb +11 -27
  25. data/lib/action_policy/policy/aliases.rb +10 -2
  26. data/lib/action_policy/policy/authorization.rb +2 -2
  27. data/lib/action_policy/policy/cache.rb +2 -2
  28. data/lib/action_policy/policy/core.rb +9 -2
  29. data/lib/action_policy/policy/pre_check.rb +2 -2
  30. data/lib/action_policy/policy/reasons.rb +2 -2
  31. data/lib/action_policy/policy/scoping.rb +2 -2
  32. data/lib/action_policy/test_helper.rb +1 -0
  33. data/lib/action_policy/utils/pretty_print.rb +2 -2
  34. data/lib/action_policy/version.rb +1 -1
  35. metadata +10 -10
  36. data/lib/.rbnext/3.0/action_policy/behaviour.rb +0 -115
  37. data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +0 -160
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7922156f6a9b0fa40a6451b6f6c114a0ea8d7f52d9d050ecfd266a5575357c6e
4
- data.tar.gz: d38c700872a91ddad81a4d2db99cb4415d4ccf51064ec10db10771ebcf517c5f
3
+ metadata.gz: 021e6da5ccd46f76732cb52c0c04ae1a45ce3a74a2b64643e179ee16c40417e3
4
+ data.tar.gz: 96a978308fde0160f5b739d47abdd833005ba7109e97837ac0dd5721f1ec52a8
5
5
  SHA512:
6
- metadata.gz: efcd8c5cd621c6b08c713e71e37416e1b88a400e1f4195039059bb6d1d8baf7d09edb9425ebc0d2bdbc3a64ae6c94f78c39b55017bd918bedf890a6a107ad5b6
7
- data.tar.gz: ca147c91c0da934f98787a4ec8ed0a84173363c828729d4d8cddb0df11a0ec4895efb63d7d25b3b9a969e807d338063ddf4746e9b4156211d96e5d8ff696a272
6
+ metadata.gz: 69953fedcbaa8d007c3c922e17dd26618eda2ed840d59bac476111f62bb7aae7c20f2920606925eb4b3acab7e90f994a6e1a9a55b7de0a0740f3747bb4e85aa3
7
+ data.tar.gz: 23de3c82949db85f3a41f079187493087add27f88e0cd9315ccb492e925f4490390d5ae17fb493eb3ae290f5469f216ab258e21e7c155be9853de5728c8b3311
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.5.7 (2021-03-03)
6
+
7
+ The previous release had incorrect dependencies (due to the missing transpiled files).
8
+
9
+ ## ~~0.5.6 (2021-03-03)~~
10
+
11
+ - Add `ActionPolicy.enforce_predicate_rules_naming` config to catch rule missing question mark ([@skojin][])
12
+
13
+ ## 0.5.5 (2020-12-28)
14
+
15
+ - Upgrade to Ruby 3.0. ([@palkan][])
16
+
17
+ ## 0.5.4 (2020-12-09)
18
+
19
+ - Add support for RSpec aliases detection when linting policy specs with `rubocop-rspec` 2.0 ([@pirj][])
20
+
21
+ - Fix `strict_namespace: true` lookup option not finding policies in global namespace ([@Be-ngt-oH][])
22
+
5
23
  ## 0.5.0 (2020-09-29)
6
24
 
7
25
  - Move `deny!` / `allow!` to core. ([@palkan][])
@@ -37,7 +55,7 @@ This method is similar to `allowed_to?` but returns an authorization result obje
37
55
 
38
56
  Fixes [#122](https://github.com/palkan/action_policy/issues/122).
39
57
 
40
- - Separated `#classify`-based and `#camelize`-based symbol lookups. ([Be-ngt-oH][])
58
+ - Separated `#classify`-based and `#camelize`-based symbol lookups. ([@Be-ngt-oH][])
41
59
 
42
60
  Only affects Rails apps. Now lookup for `:users` tries to find `UsersPolicy` first (camelize),
43
61
  and only then search for `UserPolicy` (classify).
@@ -425,7 +443,9 @@ This value is now stored in a cache (if any) instead of just the call result (`t
425
443
  [@ilyasgaraev]: https://github.com/ilyasgaraev
426
444
  [@brendon]: https://github.com/brendon
427
445
  [@DmitryTsepelev]: https://github.com/DmitryTsepelev
428
- [@korolvs]: https://github.com/korolvs
446
+ [@korolvs]: https://github.com/slavadev
429
447
  [@nicolas-brousse]: https://github.com/nicolas-brousse
430
448
  [@somenugget]: https://github.com/somenugget
431
449
  [@Be-ngt-oH]: https://github.com/Be-ngt-oH
450
+ [@pirj]: https://github.com/pirj
451
+ [@skojin]: https://github.com/skojin
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2020 Vladimir Dementyev
3
+ Copyright (c) 2018-2021 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -31,7 +31,7 @@ Composable. Extensible. Performant.
31
31
  Add this line to your application's `Gemfile`:
32
32
 
33
33
  ```ruby
34
- gem "action_policy", "~> 0.4.0"
34
+ gem "action_policy"
35
35
  ```
36
36
 
37
37
  And then execute:
@@ -0,0 +1,17 @@
1
+ RSpec:
2
+ Language:
3
+ ExampleGroups:
4
+ Regular:
5
+ - describe_rule
6
+ Focused:
7
+ - fdescribe_rule
8
+ Skipped:
9
+ - xdescribe_rule
10
+ Includes:
11
+ Examples:
12
+ - succeed
13
+ - failed
14
+ - fsucceed
15
+ - ffailed
16
+ - xsucceed
17
+ - xfailed
@@ -11,7 +11,7 @@ module ActionPolicy
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
12
12
  policy_class = with || ::ActionPolicy.lookup(
13
13
  record,
14
- **{namespace: namespace, context: context, allow_nil: allow_nil, default: default}
14
+ namespace: namespace, context: context, allow_nil: allow_nil, default: default
15
15
  )
16
16
  policy_class&.new(record, **context)
17
17
  end
@@ -19,11 +19,11 @@ module ActionPolicy
19
19
  type ||= authorization_scope_type_for(policy, target)
20
20
  name = as
21
21
 
22
- Authorizer.scopify(target, policy, **{type: type, name: name, scope_options: scope_options})
22
+ Authorizer.scopify(target, policy, type: type, name: name, scope_options: scope_options)
23
23
  end
24
24
 
25
25
  # For backward compatibility
26
- alias authorized authorized_scope
26
+ alias_method :authorized, :authorized_scope
27
27
 
28
28
  # Infer scope type for target if none provided.
29
29
  # Raises an exception if type couldn't be inferred.
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ old_verbose = $VERBOSE
4
+
5
+ begin
6
+ require "method_source"
7
+ # Ignore parse warnings when patch
8
+ # Ruby version mismatches
9
+ $VERBOSE = nil
10
+ require "parser/current"
11
+ require "unparser"
12
+ rescue LoadError
13
+ # do nothing
14
+ ensure
15
+ $VERBOSE = old_verbose
16
+ end
17
+
18
+ module ActionPolicy
19
+ using RubyNext
20
+
21
+ # Takes the object and a method name,
22
+ # and returns the "annotated" source code for the method:
23
+ # code is split into parts by logical operators and each
24
+ # part is evaluated separately.
25
+ #
26
+ # Example:
27
+ #
28
+ # class MyClass
29
+ # def access?
30
+ # admin? && access_feed?
31
+ # end
32
+ # end
33
+ #
34
+ # puts PrettyPrint.format_method(MyClass.new, :access?)
35
+ #
36
+ # #=> MyClass#access?
37
+ # #=> ↳ admin? #=> false
38
+ # #=> AND
39
+ # #=> access_feed? #=> true
40
+ module PrettyPrint
41
+ TRUE = "\e[32mtrue\e[0m"
42
+ FALSE = "\e[31mfalse\e[0m"
43
+
44
+ class Visitor
45
+ attr_reader :lines, :object
46
+ attr_accessor :indent
47
+
48
+ def initialize(object)
49
+ @object = object
50
+ end
51
+
52
+ def collect(ast)
53
+ @lines = []
54
+ @indent = 0
55
+
56
+ visit_node(ast)
57
+
58
+ lines.join("\n")
59
+ end
60
+
61
+ def visit_node(ast)
62
+ if respond_to?("visit_#{ast.type}")
63
+ send("visit_#{ast.type}", ast)
64
+ else
65
+ visit_missing ast
66
+ end
67
+ end
68
+
69
+ def expression_with_result(sexp)
70
+ expression = Unparser.unparse(sexp)
71
+ "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
+ end
73
+
74
+ def eval_exp(exp)
75
+ return "<skipped>" if ignore_exp?(exp)
76
+ object.instance_eval(exp)
77
+ rescue => e
78
+ "Failed: #{e.message}"
79
+ end
80
+
81
+ def visit_and(ast)
82
+ visit_node(ast.children[0])
83
+ lines << indented("AND")
84
+ visit_node(ast.children[1])
85
+ end
86
+
87
+ def visit_or(ast)
88
+ visit_node(ast.children[0])
89
+ lines << indented("OR")
90
+ visit_node(ast.children[1])
91
+ end
92
+
93
+ def visit_begin(ast)
94
+ # Parens
95
+ if ast.children.size == 1
96
+ lines << indented("(")
97
+ self.indent += 2
98
+ visit_node(ast.children[0])
99
+ self.indent -= 2
100
+ lines << indented(")")
101
+ else
102
+ # Multiple expressions
103
+ ast.children.each do |node|
104
+ visit_node(node)
105
+ # restore indent after each expression
106
+ self.indent -= 2
107
+ end
108
+ end
109
+ end
110
+
111
+ def visit_missing(ast)
112
+ lines << indented(expression_with_result(ast))
113
+ end
114
+
115
+ def indented(str)
116
+ "#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
117
+ # increase indent after the first expression
118
+ self.indent += 2 if indent.zero?
119
+ end
120
+ end
121
+
122
+ # Some lines should not be evaled
123
+ def ignore_exp?(exp)
124
+ PrettyPrint.ignore_expressions.any? { exp.match?(_1) }
125
+ end
126
+ end
127
+
128
+ class << self
129
+ attr_accessor :ignore_expressions
130
+
131
+ if defined?(::Unparser) && defined?(::MethodSource)
132
+ def available?() = true
133
+
134
+ def print_method(object, method_name)
135
+ ast = object.method(method_name).source.then(&Unparser.method(:parse))
136
+ # outer node is a method definition itself
137
+ body = ast.children[2]
138
+
139
+ Visitor.new(object).collect(body)
140
+ end
141
+ else
142
+ def available?() = false
143
+
144
+ def print_method(_, _) = ""
145
+ end
146
+
147
+ def colorize(val)
148
+ return val unless $stdout.isatty
149
+ return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
150
+ return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
151
+ val
152
+ end
153
+ end
154
+
155
+ self.ignore_expressions = [
156
+ /^\s*binding\.(pry|irb)\s*$/s
157
+ ]
158
+ end
159
+ end
@@ -11,7 +11,7 @@ module ActionPolicy
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
12
12
  policy_class = with || ::ActionPolicy.lookup(
13
13
  record,
14
- **{namespace: namespace, context: context, allow_nil: allow_nil, default: default}
14
+ namespace: namespace, context: context, allow_nil: allow_nil, default: default
15
15
  )
16
16
  policy_class&.new(record, **context)
17
17
  end
@@ -146,8 +146,8 @@ module ActionPolicy
146
146
 
147
147
  def colorize(val)
148
148
  return val unless $stdout.isatty
149
- return TRUE if val.eql?(true)
150
- return FALSE if val.eql?(false)
149
+ return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
150
+ return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
151
151
  val
152
152
  end
153
153
  end
@@ -40,7 +40,7 @@ module ActionPolicy
40
40
  base.prepend InstanceMethods
41
41
  end
42
42
 
43
- alias included prepended
43
+ alias_method :included, :prepended
44
44
  end
45
45
 
46
46
  module InstanceMethods # :nodoc:
@@ -31,10 +31,18 @@ module ActionPolicy
31
31
  def resolve_rule(activity)
32
32
  self.class.lookup_alias(activity) ||
33
33
  (activity if respond_to?(activity)) ||
34
+ (check_rule_naming(activity) if ActionPolicy.enforce_predicate_rules_naming) ||
34
35
  self.class.lookup_default_rule ||
35
36
  super
36
37
  end
37
38
 
39
+ private def check_rule_naming(activity)
40
+ unless activity[-1] == "?"
41
+ raise NonPredicateRule.new(self, activity)
42
+ end
43
+ nil
44
+ end
45
+
38
46
  module ClassMethods # :nodoc:
39
47
  def default_rule(val)
40
48
  rules_aliases[DEFAULT] = val
@@ -23,12 +23,19 @@ module ActionPolicy
23
23
  def initialize(policy, rule)
24
24
  @policy = policy.class
25
25
  @rule = rule
26
- @message =
27
- "Couldn't find rule '#{@rule}' for #{@policy}" \
26
+ @message = "Couldn't find rule '#{@rule}' for #{@policy}" \
28
27
  "#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
29
28
  end
30
29
  end
31
30
 
31
+ class NonPredicateRule < UnknownRule
32
+ def initialize(policy, rule)
33
+ @policy = policy.class
34
+ @rule = rule
35
+ @message = "The rule '#{@rule}' of '#{@policy}' must ends with ? (question mark)\nDid you mean? #{@rule}?"
36
+ end
37
+ end
38
+
32
39
  module Policy
33
40
  # Core policy API
34
41
  module Core
@@ -185,10 +185,12 @@ module ActionPolicy
185
185
  def allowed_to?(rule, record = :__undef__, **options)
186
186
  res =
187
187
  if (record == :__undef__ || record == self.record) && options.empty?
188
+ rule = resolve_rule(rule)
188
189
  policy = self
189
190
  with_clean_result { apply(rule) }
190
191
  else
191
192
  policy = policy_for(record: record, **options)
193
+ rule = policy.resolve_rule(rule)
192
194
 
193
195
  policy.apply(rule)
194
196
  policy.result
@@ -146,8 +146,8 @@ module ActionPolicy
146
146
 
147
147
  def colorize(val)
148
148
  return val unless $stdout.isatty
149
- return TRUE if val.eql?(true)
150
- return FALSE if val.eql?(false)
149
+ return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
150
+ return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
151
151
  val
152
152
  end
153
153
  end
data/lib/action_policy.rb CHANGED
@@ -34,6 +34,8 @@ module ActionPolicy
34
34
  class << self
35
35
  attr_accessor :cache_store
36
36
 
37
+ attr_accessor :enforce_predicate_rules_naming
38
+
37
39
  # Find a policy class for a target
38
40
  def lookup(target, allow_nil: false, default: nil, **options)
39
41
  LookupChain.call(target, **options) ||
@@ -104,11 +104,11 @@ module ActionPolicy
104
104
  def authorization_targets
105
105
  return @authorization_targets if instance_variable_defined?(:@authorization_targets)
106
106
 
107
- if superclass.respond_to?(:authorization_targets)
107
+ @authorization_targets = if superclass.respond_to?(:authorization_targets)
108
108
  superclass.authorization_targets.dup
109
109
  else
110
110
  {}
111
- end => @authorization_targets
111
+ end
112
112
  end
113
113
  end
114
114
  end
@@ -24,7 +24,7 @@ module ActionPolicy
24
24
  base.prepend InstanceMethods
25
25
  end
26
26
 
27
- alias included prepended
27
+ alias_method :included, :prepended
28
28
  end
29
29
 
30
30
  module InstanceMethods # :nodoc:
@@ -66,7 +66,7 @@ module ActionPolicy
66
66
  base.prepend InstanceMethods
67
67
  end
68
68
 
69
- alias included prepended
69
+ alias_method :included, :prepended
70
70
  end
71
71
 
72
72
  module InstanceMethods # :nodoc:
@@ -11,7 +11,7 @@ module ActionPolicy
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
12
12
  policy_class = with || ::ActionPolicy.lookup(
13
13
  record,
14
- **{namespace, context, allow_nil, default}
14
+ namespace:, context:, allow_nil:, default:
15
15
  )
16
16
  policy_class&.new(record, **context)
17
17
  end
@@ -19,11 +19,11 @@ module ActionPolicy
19
19
  type ||= authorization_scope_type_for(policy, target)
20
20
  name = as
21
21
 
22
- Authorizer.scopify(target, policy, **{type, name, scope_options})
22
+ Authorizer.scopify(target, policy, type:, name:, scope_options:)
23
23
  end
24
24
 
25
25
  # For backward compatibility
26
- alias authorized authorized_scope
26
+ alias_method :authorized, :authorized_scope
27
27
 
28
28
  # Infer scope type for target if none provided.
29
29
  # Raises an exception if type couldn't be inferred.
@@ -40,7 +40,7 @@ module ActionPolicy
40
40
  base.prepend InstanceMethods
41
41
  end
42
42
 
43
- alias included prepended
43
+ alias_method :included, :prepended
44
44
  end
45
45
 
46
46
  module InstanceMethods # :nodoc:
@@ -36,8 +36,7 @@ module ActionPolicy
36
36
  if strict
37
37
  put_if_absent(:strict, namespace, policy, &block)
38
38
  else
39
- cached_policy = put_if_absent(:strict, namespace, policy, &block)
40
- put_if_absent(:flexible, namespace, policy) { cached_policy }
39
+ put_if_absent(:flexible, namespace, policy, &block)
41
40
  end
42
41
  end
43
42
 
@@ -53,7 +52,7 @@ module ActionPolicy
53
52
  class << self
54
53
  attr_accessor :chain, :namespace_cache_enabled
55
54
 
56
- alias namespace_cache_enabled? namespace_cache_enabled
55
+ alias_method :namespace_cache_enabled?, :namespace_cache_enabled
57
56
 
58
57
  def call(record, **opts)
59
58
  chain.each do |probe|
@@ -66,22 +65,17 @@ module ActionPolicy
66
65
  private
67
66
 
68
67
  def lookup_within_namespace(policy_name, namespace, strict: false)
69
- return unless namespace
70
- NamespaceCache.fetch(namespace.name, policy_name, strict: strict) do
68
+ NamespaceCache.fetch(namespace&.name || "Kernel", policy_name, strict: strict) do
71
69
  mod = namespace
72
70
  loop do
73
- policy = "#{mod.name}::#{policy_name}".safe_constantize
74
- break policy unless policy.nil?
71
+ policy = [mod&.name, policy_name].compact.join("::").safe_constantize
72
+ break policy if policy || mod.nil? || strict
73
+
75
74
  mod = mod.namespace
76
- break if mod.nil? || strict
77
75
  end
78
76
  end
79
77
  end
80
78
 
81
- def objectify_policy(policy_name, strict: false)
82
- policy_name.safe_constantize unless strict
83
- end
84
-
85
79
  def policy_class_name_for(record)
86
80
  return record.policy_name.to_s if record.respond_to?(:policy_name)
87
81
 
@@ -110,17 +104,10 @@ module ActionPolicy
110
104
  record.class.policy_class if record.class.respond_to?(:policy_class)
111
105
  }
112
106
 
113
- # Lookup within namespace when provided
114
- NAMESPACE_LOOKUP = ->(record, namespace: nil, **) {
115
- next if namespace.nil?
116
-
117
- policy_name = policy_class_name_for(record)
118
- lookup_within_namespace(policy_name, namespace)
119
- }
120
-
121
107
  # Infer from class name
122
- INFER_FROM_CLASS = ->(record, **) {
123
- policy_class_name_for(record).safe_constantize
108
+ INFER_FROM_CLASS = ->(record, namespace: nil, strict_namespace: false, **) {
109
+ policy_name = policy_class_name_for(record)
110
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
124
111
  }
125
112
 
126
113
  # Infer from passed symbol
@@ -128,8 +115,7 @@ module ActionPolicy
128
115
  next unless record.is_a?(Symbol)
129
116
 
130
117
  policy_name = "#{record.camelize}Policy"
131
- lookup_within_namespace(policy_name, namespace, strict: strict_namespace) ||
132
- objectify_policy(policy_name, strict: strict_namespace)
118
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
133
119
  }
134
120
 
135
121
  # (Optional) Infer using String#classify if available
@@ -137,8 +123,7 @@ module ActionPolicy
137
123
  next unless record.is_a?(Symbol)
138
124
 
139
125
  policy_name = "#{record.to_s.classify}Policy"
140
- lookup_within_namespace(policy_name, namespace, strict: strict_namespace) ||
141
- objectify_policy(policy_name, strict: strict_namespace)
126
+ lookup_within_namespace(policy_name, namespace, strict: strict_namespace)
142
127
  }
143
128
 
144
129
  self.chain = [
@@ -146,7 +131,6 @@ module ActionPolicy
146
131
  (CLASSIFY_SYMBOL_LOOKUP if String.method_defined?(:classify)),
147
132
  INSTANCE_POLICY_CLASS,
148
133
  CLASS_POLICY_CLASS,
149
- NAMESPACE_LOOKUP,
150
134
  INFER_FROM_CLASS
151
135
  ].compact
152
136
  end
@@ -31,10 +31,18 @@ module ActionPolicy
31
31
  def resolve_rule(activity)
32
32
  self.class.lookup_alias(activity) ||
33
33
  (activity if respond_to?(activity)) ||
34
+ (check_rule_naming(activity) if ActionPolicy.enforce_predicate_rules_naming) ||
34
35
  self.class.lookup_default_rule ||
35
36
  super
36
37
  end
37
38
 
39
+ private def check_rule_naming(activity)
40
+ unless activity[-1] == "?"
41
+ raise NonPredicateRule.new(self, activity)
42
+ end
43
+ nil
44
+ end
45
+
38
46
  module ClassMethods # :nodoc:
39
47
  def default_rule(val)
40
48
  rules_aliases[DEFAULT] = val
@@ -53,11 +61,11 @@ module ActionPolicy
53
61
  def rules_aliases
54
62
  return @rules_aliases if instance_variable_defined?(:@rules_aliases)
55
63
 
56
- if superclass.respond_to?(:rules_aliases)
64
+ @rules_aliases = if superclass.respond_to?(:rules_aliases)
57
65
  superclass.rules_aliases.dup
58
66
  else
59
67
  {}
60
- end => @rules_aliases
68
+ end
61
69
  end
62
70
 
63
71
  def method_added(name)
@@ -75,11 +75,11 @@ module ActionPolicy
75
75
  def authorization_targets
76
76
  return @authorization_targets if instance_variable_defined?(:@authorization_targets)
77
77
 
78
- if superclass.respond_to?(:authorization_targets)
78
+ @authorization_targets = if superclass.respond_to?(:authorization_targets)
79
79
  superclass.authorization_targets.dup
80
80
  else
81
81
  {}
82
- end => @authorization_targets
82
+ end
83
83
  end
84
84
  end
85
85
  end
@@ -89,11 +89,11 @@ module ActionPolicy # :nodoc:
89
89
  def cached_rules
90
90
  return @cached_rules if instance_variable_defined?(:@cached_rules)
91
91
 
92
- if superclass.respond_to?(:cached_rules)
92
+ @cached_rules = if superclass.respond_to?(:cached_rules)
93
93
  superclass.cached_rules.dup
94
94
  else
95
95
  {}
96
- end => @cached_rules
96
+ end
97
97
  end
98
98
  end
99
99
  end
@@ -23,12 +23,19 @@ module ActionPolicy
23
23
  def initialize(policy, rule)
24
24
  @policy = policy.class
25
25
  @rule = rule
26
- @message =
27
- "Couldn't find rule '#{@rule}' for #{@policy}" \
26
+ @message = "Couldn't find rule '#{@rule}' for #{@policy}" \
28
27
  "#{suggest(@rule, @policy.instance_methods - Object.instance_methods)}"
29
28
  end
30
29
  end
31
30
 
31
+ class NonPredicateRule < UnknownRule
32
+ def initialize(policy, rule)
33
+ @policy = policy.class
34
+ @rule = rule
35
+ @message = "The rule '#{@rule}' of '#{@policy}' must ends with ? (question mark)\nDid you mean? #{@rule}?"
36
+ end
37
+ end
38
+
32
39
  module Policy
33
40
  # Core policy API
34
41
  module Core
@@ -150,11 +150,11 @@ module ActionPolicy
150
150
  def pre_checks
151
151
  return @pre_checks if instance_variable_defined?(:@pre_checks)
152
152
 
153
- if superclass.respond_to?(:pre_checks)
153
+ @pre_checks = if superclass.respond_to?(:pre_checks)
154
154
  superclass.pre_checks.dup
155
155
  else
156
156
  []
157
- end => @pre_checks
157
+ end
158
158
  end
159
159
  end
160
160
  end
@@ -72,7 +72,7 @@ module ActionPolicy
72
72
  def all_details
73
73
  return @all_details if defined?(@all_details)
74
74
 
75
- {}.tap do |all|
75
+ @all_details = {}.tap do |all|
76
76
  next unless defined?(@reasons)
77
77
 
78
78
  reasons.reasons.each_value do |rules|
@@ -84,7 +84,7 @@ module ActionPolicy
84
84
  all.merge!(details)
85
85
  end
86
86
  end
87
- end => @all_details
87
+ end
88
88
  end
89
89
 
90
90
  # Add reasons to inspect
@@ -148,11 +148,11 @@ module ActionPolicy
148
148
  def scope_matchers
149
149
  return @scope_matchers if instance_variable_defined?(:@scope_matchers)
150
150
 
151
- if superclass.respond_to?(:scope_matchers)
151
+ @scope_matchers = if superclass.respond_to?(:scope_matchers)
152
152
  superclass.scope_matchers.dup
153
153
  else
154
154
  []
155
- end => @scope_matchers
155
+ end
156
156
  end
157
157
  end
158
158
  end
@@ -21,6 +21,7 @@ module ActionPolicy
21
21
  yield scopes.first.target
22
22
  end
23
23
  end
24
+
24
25
  # Asserts that the given policy was used to authorize the given target.
25
26
  #
26
27
  # def test_authorize
@@ -146,8 +146,8 @@ module ActionPolicy
146
146
 
147
147
  def colorize(val)
148
148
  return val unless $stdout.isatty
149
- return TRUE if val.eql?(true)
150
- return FALSE if val.eql?(false)
149
+ return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
150
+ return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
151
151
  val
152
152
  end
153
153
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.5.2"
4
+ VERSION = "0.5.7"
5
5
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-01 00:00:00.000000000 Z
11
+ date: 2021-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ruby-next
14
+ name: ruby-next-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.10.3
19
+ version: 0.11.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.10.3
26
+ version: 0.11.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: ammeter
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +132,11 @@ files:
132
132
  - CHANGELOG.md
133
133
  - LICENSE.txt
134
134
  - README.md
135
+ - config/rubocop-rspec.yml
136
+ - lib/.rbnext/1995.next/action_policy/behaviours/policy_for.rb
137
+ - lib/.rbnext/1995.next/action_policy/behaviours/scoping.rb
138
+ - lib/.rbnext/1995.next/action_policy/policy/authorization.rb
139
+ - lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb
135
140
  - lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
136
141
  - lib/.rbnext/2.7/action_policy/i18n.rb
137
142
  - lib/.rbnext/2.7/action_policy/policy/cache.rb
@@ -139,20 +144,15 @@ files:
139
144
  - lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
140
145
  - lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
141
146
  - lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
142
- - lib/.rbnext/3.0/action_policy/behaviour.rb
143
- - lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
144
- - lib/.rbnext/3.0/action_policy/behaviours/scoping.rb
145
147
  - lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
146
148
  - lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
147
149
  - lib/.rbnext/3.0/action_policy/policy/aliases.rb
148
- - lib/.rbnext/3.0/action_policy/policy/authorization.rb
149
150
  - lib/.rbnext/3.0/action_policy/policy/cache.rb
150
151
  - lib/.rbnext/3.0/action_policy/policy/core.rb
151
152
  - lib/.rbnext/3.0/action_policy/policy/defaults.rb
152
153
  - lib/.rbnext/3.0/action_policy/policy/execution_result.rb
153
154
  - lib/.rbnext/3.0/action_policy/policy/pre_check.rb
154
155
  - lib/.rbnext/3.0/action_policy/policy/reasons.rb
155
- - lib/.rbnext/3.0/action_policy/policy/scoping.rb
156
156
  - lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb
157
157
  - lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb
158
158
  - lib/.rbnext/3.0/action_policy/utils/pretty_print.rb
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy/behaviours/policy_for"
4
- require "action_policy/behaviours/scoping"
5
- require "action_policy/behaviours/memoized"
6
- require "action_policy/behaviours/thread_memoized"
7
- require "action_policy/behaviours/namespaced"
8
-
9
- require "action_policy/authorizer"
10
-
11
- module ActionPolicy
12
- # Provides `authorize!` and `allowed_to?` methods and
13
- # `authorize` class method to define authorization context.
14
- #
15
- # Could be included anywhere to perform authorization.
16
- module Behaviour
17
- include ActionPolicy::Behaviours::PolicyFor
18
- include ActionPolicy::Behaviours::Scoping
19
-
20
- def self.included(base)
21
- # Handle ActiveSupport::Concern differently
22
- if base.respond_to?(:class_methods)
23
- base.class_methods do
24
- include ClassMethods
25
- end
26
- else
27
- base.extend ClassMethods
28
- end
29
- end
30
-
31
- # Authorize action against a policy.
32
- #
33
- # Policy is inferred from record
34
- # (unless explicitly specified through `with` option).
35
- #
36
- # Raises `ActionPolicy::Unauthorized` if check failed.
37
- def authorize!(record = :__undef__, to:, **options)
38
- policy = lookup_authorization_policy(record, **options)
39
-
40
- Authorizer.call(policy, authorization_rule_for(policy, to))
41
- end
42
-
43
- # Checks that an activity is allowed for the current context (e.g. user).
44
- #
45
- # Returns true of false.
46
- def allowed_to?(rule, record = :__undef__, **options)
47
- policy = lookup_authorization_policy(record, **options)
48
-
49
- policy.apply(authorization_rule_for(policy, rule))
50
- end
51
-
52
- # Returns the authorization result object after applying a specified rule to a record.
53
- def allowance_to(rule, record = :__undef__, **options)
54
- policy = lookup_authorization_policy(record, **options)
55
-
56
- policy.apply(authorization_rule_for(policy, rule))
57
- policy.result
58
- end
59
-
60
- def authorization_context
61
- return @__authorization_context if
62
- instance_variable_defined?(:@__authorization_context)
63
-
64
- @__authorization_context = self.class.authorization_targets
65
- .each_with_object({}) do |(key, meth), obj|
66
- obj[key] = send(meth)
67
- end
68
- end
69
-
70
- # Check that rule is defined for policy,
71
- # otherwise fallback to :manage? rule.
72
- def authorization_rule_for(policy, rule)
73
- policy.resolve_rule(rule)
74
- end
75
-
76
- def lookup_authorization_policy(record, **options) # :nodoc:
77
- record = implicit_authorization_target! if record == :__undef__
78
- raise ArgumentError, "Record must be specified" if record.nil?
79
-
80
- options[:context] && (options[:context] = authorization_context.merge(options[:context]))
81
-
82
- policy_for(record: record, **options)
83
- end
84
-
85
- module ClassMethods # :nodoc:
86
- # Configure authorization context.
87
- #
88
- # For example:
89
- #
90
- # class ApplicationController < ActionController::Base
91
- # # Pass the value of `current_user` to authorization as `user`
92
- # authorize :user, through: :current_user
93
- # end
94
- #
95
- # # Assuming that in your ApplicationPolicy
96
- # class ApplicationPolicy < ActionPolicy::Base
97
- # authorize :user
98
- # end
99
- def authorize(key, through: nil)
100
- meth = through || key
101
- authorization_targets[key] = meth
102
- end
103
-
104
- def authorization_targets
105
- return @authorization_targets if instance_variable_defined?(:@authorization_targets)
106
-
107
- @authorization_targets = if superclass.respond_to?(:authorization_targets)
108
- superclass.authorization_targets.dup
109
- else
110
- {}
111
- end
112
- end
113
- end
114
- end
115
- end
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_policy/behaviours/scoping"
4
-
5
- require "action_policy/utils/suggest_message"
6
-
7
- module ActionPolicy
8
- class UnknownScopeType < Error # :nodoc:
9
- include ActionPolicy::SuggestMessage
10
-
11
- MESSAGE_TEMPLATE = "Unknown policy scope type :%s for %s%s"
12
-
13
- attr_reader :message
14
-
15
- def initialize(policy_class, type)
16
- @message = format(
17
- MESSAGE_TEMPLATE,
18
- type, policy_class,
19
- suggest(type, policy_class.scoping_handlers.keys)
20
- )
21
- end
22
- end
23
-
24
- class UnknownNamedScope < Error # :nodoc:
25
- include ActionPolicy::SuggestMessage
26
-
27
- MESSAGE_TEMPLATE = "Unknown named scope :%s for type :%s for %s%s"
28
-
29
- attr_reader :message
30
-
31
- def initialize(policy_class, type, name)
32
- @message = format(
33
- MESSAGE_TEMPLATE, name, type, policy_class,
34
- suggest(name, policy_class.scoping_handlers[type].keys)
35
- )
36
- end
37
- end
38
-
39
- class UnrecognizedScopeTarget < Error # :nodoc:
40
- MESSAGE_TEMPLATE = "Couldn't infer scope type for %s instance"
41
-
42
- attr_reader :message
43
-
44
- def initialize(target)
45
- target_class = target.is_a?(Module) ? target : target.class
46
-
47
- @message = format(
48
- MESSAGE_TEMPLATE, target_class
49
- )
50
- end
51
- end
52
-
53
- module Policy
54
- # Scoping is used to modify the _object under authorization_.
55
- #
56
- # The most common situation is when you want to _scope_ the collection depending
57
- # on the current user permissions.
58
- #
59
- # For example:
60
- #
61
- # class ApplicationPolicy < ActionPolicy::Base
62
- # # Scoping only makes sense when you have the authorization context
63
- # authorize :user
64
- #
65
- # # :relation here is a scoping type
66
- # scope_for :relation do |relation|
67
- # # authorization context is available within a scope
68
- # if user.admin?
69
- # relation
70
- # else
71
- # relation.publicly_visible
72
- # end
73
- # end
74
- # end
75
- #
76
- # base_scope = User.all
77
- # authorized_scope = ApplicantPolicy.new(user: user)
78
- # .apply_scope(base_scope, type: :relation)
79
- module Scoping
80
- class << self
81
- def included(base)
82
- base.extend ClassMethods
83
- end
84
- end
85
-
86
- include ActionPolicy::Behaviours::Scoping
87
-
88
- # Pass target to the scope handler of the specified type and name.
89
- # If `name` is not specified then `:default` name is used.
90
- # If `type` is not specified then we try to infer the type from the
91
- # target class.
92
- def apply_scope(target, type:, name: :default, scope_options: nil)
93
- raise ActionPolicy::UnknownScopeType.new(self.class, type) unless
94
- self.class.scoping_handlers.key?(type)
95
-
96
- raise ActionPolicy::UnknownNamedScope.new(self.class, type, name) unless
97
- self.class.scoping_handlers[type].key?(name)
98
-
99
- mid = :"__scoping__#{type}__#{name}"
100
- scope_options ? send(mid, target, **scope_options) : send(mid, target)
101
- end
102
-
103
- def resolve_scope_type(target)
104
- lookup_type_from_target(target) ||
105
- raise(ActionPolicy::UnrecognizedScopeTarget, target)
106
- end
107
-
108
- def lookup_type_from_target(target)
109
- self.class.scope_matchers.detect do |(_type, matcher)|
110
- matcher === target
111
- end&.first
112
- end
113
-
114
- module ClassMethods # :nodoc:
115
- # Register a new scoping method for the `type`
116
- def scope_for(type, name = :default, &block)
117
- mid = :"__scoping__#{type}__#{name}"
118
-
119
- define_method(mid, &block)
120
-
121
- scoping_handlers[type][name] = mid
122
- end
123
-
124
- def scoping_handlers
125
- return @scoping_handlers if instance_variable_defined?(:@scoping_handlers)
126
-
127
- @scoping_handlers =
128
- Hash.new { |h, k| h[k] = {} }.tap do |handlers|
129
- if superclass.respond_to?(:scoping_handlers)
130
- superclass.scoping_handlers.each do |k, v|
131
- handlers[k] = v.dup
132
- end
133
- end
134
- end
135
- end
136
-
137
- # Define scope type matcher.
138
- #
139
- # Scope matcher is an object that implements `#===` (_case equality_) or a Proc.
140
- #
141
- # When no type is provided when applying a scope we try to infer a type
142
- # from the target object by calling matchers one by one until we find a matching
143
- # type (i.e. there is a matcher which returns `true` when applying it to the target).
144
- def scope_matcher(type, class_or_proc)
145
- scope_matchers << [type, class_or_proc]
146
- end
147
-
148
- def scope_matchers
149
- return @scope_matchers if instance_variable_defined?(:@scope_matchers)
150
-
151
- @scope_matchers = if superclass.respond_to?(:scope_matchers)
152
- superclass.scope_matchers.dup
153
- else
154
- []
155
- end
156
- end
157
- end
158
- end
159
- end
160
- end