action_policy 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -1
  3. data/LICENSE.txt +1 -1
  4. data/README.md +1 -1
  5. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/behaviours/policy_for.rb +10 -4
  6. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/behaviours/scoping.rb +2 -2
  7. data/lib/.rbnext/{3.0 → 1995.next}/action_policy/policy/authorization.rb +0 -0
  8. data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +159 -0
  9. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +10 -4
  10. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +2 -2
  11. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +1 -1
  12. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +8 -0
  13. data/lib/.rbnext/3.0/action_policy/policy/core.rb +10 -3
  14. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +18 -2
  15. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +2 -2
  16. data/lib/action_policy/behaviour.rb +4 -6
  17. data/lib/action_policy/behaviours/memoized.rb +1 -1
  18. data/lib/action_policy/behaviours/namespaced.rb +1 -1
  19. data/lib/action_policy/behaviours/policy_for.rb +10 -4
  20. data/lib/action_policy/behaviours/scoping.rb +2 -2
  21. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  22. data/lib/action_policy/lookup_chain.rb +1 -1
  23. data/lib/action_policy/policy/aliases.rb +10 -2
  24. data/lib/action_policy/policy/authorization.rb +2 -2
  25. data/lib/action_policy/policy/cache.rb +2 -2
  26. data/lib/action_policy/policy/core.rb +10 -3
  27. data/lib/action_policy/policy/pre_check.rb +2 -2
  28. data/lib/action_policy/policy/reasons.rb +20 -4
  29. data/lib/action_policy/policy/scoping.rb +2 -2
  30. data/lib/action_policy/rails/controller.rb +1 -1
  31. data/lib/action_policy/test_helper.rb +1 -0
  32. data/lib/action_policy/utils/pretty_print.rb +2 -2
  33. data/lib/action_policy/version.rb +1 -1
  34. data/lib/action_policy.rb +2 -0
  35. metadata +13 -14
  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: 911b1f7b929d458ae0bb95eb22fa11ff2a17925d9c797b626664bdad82b7abbc
4
- data.tar.gz: 69bc6ead609db7cbcdc3d31455e7e17b88bf9037a51a8b1722e414a7e50c41e4
3
+ metadata.gz: 0fe2b2b40f6a3bd85c312495209382b1ee72950072257e676be008e2ef8d77c5
4
+ data.tar.gz: f0c9b0cb38bc130cdb8c7f3f4cfba3adcc326f25c6435fe75723e8c0d3ba3fe9
5
5
  SHA512:
6
- metadata.gz: e3cf8e4bd9347f052a34cee11fad004030ee80d9281c6bbbb9fe99b8f6665b0c611661d6ce3be8578345ef3171ad35d2c38fff8936bdd47efef48bb137a5a415
7
- data.tar.gz: 63c4444667971ee445b60e2d9ffee89402021151ac730f00063f2066984c1ca75c15ae40b45fbce60ce9c4201826116bb417f6e62dbfd91f96a3f3974e26a458
6
+ metadata.gz: f49f02d335942aa0a100e681f7570c1b6d042e68d1ed9d02012ec84b9f354b5c0a1d7a42ba15fbd3c84c84536e444af95057a09ef00cdc6ae3c2f5d0b24dd3a6
7
+ data.tar.gz: 2eced066c406feb11e8cdf99113fa8d27f8fe73ff015bcc1d2958c0a657540d195b25a8328dee02206b8539e55d16278f531dcde6b9567fef95ec1bead92cda0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.6.0 (2021-09-02)
6
+
7
+ - Drop Ruby 2.5 support.
8
+ - [Closes [#186](https://github.com/palkan/action_policy/issues/186)] Add `inline_reasons: true` option to `allowed_to?` to avoid wrapping reasons. ([@palkan][])
9
+ - [Fixes [#173](https://github.com/palkan/action_policy/issues/173)] Explicit context were not merged with implicit one within policy classes. ([@palkan][])
10
+ - Add `strict_namespace:` option to policy_for behaviour ([@kevynlebouille][])
11
+ - Prevent possible side effects in policy lookup ([@tomdalling][])
12
+
13
+ ## 0.5.7 (2021-03-03)
14
+
15
+ The previous release had incorrect dependencies (due to the missing transpiled files).
16
+
17
+ ## ~~0.5.6 (2021-03-03)~~
18
+
19
+ - Add `ActionPolicy.enforce_predicate_rules_naming` config to catch rule missing question mark ([@skojin][])
20
+
21
+ ## 0.5.5 (2020-12-28)
22
+
23
+ - Upgrade to Ruby 3.0. ([@palkan][])
24
+
5
25
  ## 0.5.4 (2020-12-09)
6
26
 
7
27
  - Add support for RSpec aliases detection when linting policy specs with `rubocop-rspec` 2.0 ([@pirj][])
@@ -431,8 +451,10 @@ This value is now stored in a cache (if any) instead of just the call result (`t
431
451
  [@ilyasgaraev]: https://github.com/ilyasgaraev
432
452
  [@brendon]: https://github.com/brendon
433
453
  [@DmitryTsepelev]: https://github.com/DmitryTsepelev
434
- [@korolvs]: https://github.com/korolvs
454
+ [@korolvs]: https://github.com/slavadev
435
455
  [@nicolas-brousse]: https://github.com/nicolas-brousse
436
456
  [@somenugget]: https://github.com/somenugget
437
457
  [@Be-ngt-oH]: https://github.com/Be-ngt-oH
438
458
  [@pirj]: https://github.com/pirj
459
+ [@skojin]: https://github.com/skojin
460
+ [@tomdalling]: https://github.com/tomdalling
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:
@@ -8,16 +8,18 @@ module ActionPolicy
8
8
  using ActionPolicy::Ext::PolicyCacheKey
9
9
 
10
10
  # Returns policy instance for the record.
11
- def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
+ context = context ? authorization_context.merge(context) : authorization_context
13
+
12
14
  policy_class = with || ::ActionPolicy.lookup(
13
15
  record,
14
- **{namespace: namespace, context: context, allow_nil: allow_nil, default: default}
16
+ namespace: namespace, context: context, allow_nil: allow_nil, default: default, strict_namespace: strict_namespace
15
17
  )
16
18
  policy_class&.new(record, **context)
17
19
  end
18
20
 
19
21
  def authorization_context
20
- raise NotImplementedError, "Please, define `authorization_context` method!"
22
+ Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
23
  end
22
24
 
23
25
  def authorization_namespace
@@ -28,6 +30,10 @@ module ActionPolicy
28
30
  # override to provide a policy class use when no policy found
29
31
  end
30
32
 
33
+ def authorization_strict_namespace
34
+ # override to provide strict namespace lookup option
35
+ end
36
+
31
37
  # Override this method to provide implicit authorization target
32
38
  # that would be used in case `record` is not specified in
33
39
  # `authorize!` and `allowed_to?` call.
@@ -39,7 +45,7 @@ module ActionPolicy
39
45
 
40
46
  # Return implicit authorization target or raises an exception if it's nil
41
47
  def implicit_authorization_target!
42
- implicit_authorization_target || raise(
48
+ implicit_authorization_target || Kernel.raise(
43
49
  NotFound,
44
50
  [
45
51
  self,
@@ -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
@@ -8,16 +8,18 @@ module ActionPolicy
8
8
  using ActionPolicy::Ext::PolicyCacheKey
9
9
 
10
10
  # Returns policy instance for the record.
11
- def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
+ context = context ? authorization_context.merge(context) : authorization_context
13
+
12
14
  policy_class = with || ::ActionPolicy.lookup(
13
15
  record,
14
- **{namespace: namespace, context: context, allow_nil: allow_nil, default: default}
16
+ namespace: namespace, context: context, allow_nil: allow_nil, default: default, strict_namespace: strict_namespace
15
17
  )
16
18
  policy_class&.new(record, **context)
17
19
  end
18
20
 
19
21
  def authorization_context
20
- raise NotImplementedError, "Please, define `authorization_context` method!"
22
+ Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
23
  end
22
24
 
23
25
  def authorization_namespace
@@ -28,6 +30,10 @@ module ActionPolicy
28
30
  # override to provide a policy class use when no policy found
29
31
  end
30
32
 
33
+ def authorization_strict_namespace
34
+ # override to provide strict namespace lookup option
35
+ end
36
+
31
37
  # Override this method to provide implicit authorization target
32
38
  # that would be used in case `record` is not specified in
33
39
  # `authorize!` and `allowed_to?` call.
@@ -39,7 +45,7 @@ module ActionPolicy
39
45
 
40
46
  # Return implicit authorization target or raises an exception if it's nil
41
47
  def implicit_authorization_target!
42
- implicit_authorization_target || raise(
48
+ implicit_authorization_target || Kernel.raise(
43
49
  NotFound,
44
50
  [
45
51
  self,
@@ -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
@@ -126,7 +133,7 @@ module ActionPolicy
126
133
  end
127
134
 
128
135
  # An alias for readability purposes
129
- def check?(*args) ; allowed_to?(*args); end
136
+ def check?(*args, **hargs) ; allowed_to?(*args, **hargs); end
130
137
 
131
138
  # Returns a rule name (policy method name) for activity.
132
139
  #
@@ -31,6 +31,20 @@ module ActionPolicy
31
31
 
32
32
  def present?() ; !empty?; end
33
33
 
34
+ def merge(other)
35
+ other.reasons.each do |policy_class, rules|
36
+ reasons[policy_class] ||= []
37
+
38
+ rules.each do |rule|
39
+ if rule.is_a?(::Hash)
40
+ add_detailed_reason(reasons[policy_class], rule)
41
+ else
42
+ add_non_detailed_reason(reasons[policy_class], rule)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
34
48
  private
35
49
 
36
50
  def add_non_detailed_reason(store, rule)
@@ -182,7 +196,7 @@ module ActionPolicy
182
196
  result.details ||= {}
183
197
  end
184
198
 
185
- def allowed_to?(rule, record = :__undef__, **options)
199
+ def allowed_to?(rule, record = :__undef__, inline_reasons: false, **options)
186
200
  res =
187
201
  if (record == :__undef__ || record == self.record) && options.empty?
188
202
  rule = resolve_rule(rule)
@@ -196,7 +210,9 @@ module ActionPolicy
196
210
  policy.result
197
211
  end
198
212
 
199
- result&.reasons&.add(policy, rule, res.details) if res.fail?
213
+ if res.fail? && result&.reasons
214
+ inline_reasons ? result.reasons.merge(res.reasons) : result.reasons.add(policy, rule, res.details)
215
+ end
200
216
 
201
217
  res.clear_details
202
218
 
@@ -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
@@ -74,10 +74,8 @@ module ActionPolicy
74
74
  end
75
75
 
76
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]))
77
+ record = implicit_authorization_target! if :__undef__ == record # rubocop:disable Style/YodaCondition See https://github.com/palkan/action_policy/pull/180
78
+ Kernel.raise ArgumentError, "Record must be specified" if record.nil?
81
79
 
82
80
  policy_for(record: record, **options)
83
81
  end
@@ -104,11 +102,11 @@ module ActionPolicy
104
102
  def authorization_targets
105
103
  return @authorization_targets if instance_variable_defined?(:@authorization_targets)
106
104
 
107
- if superclass.respond_to?(:authorization_targets)
105
+ @authorization_targets = if superclass.respond_to?(:authorization_targets)
108
106
  superclass.authorization_targets.dup
109
107
  else
110
108
  {}
111
- end => @authorization_targets
109
+ end
112
110
  end
113
111
  end
114
112
  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:
@@ -8,16 +8,18 @@ module ActionPolicy
8
8
  using ActionPolicy::Ext::PolicyCacheKey
9
9
 
10
10
  # Returns policy instance for the record.
11
- def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false, default: default_authorization_policy_class)
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
+ context = context ? authorization_context.merge(context) : authorization_context
13
+
12
14
  policy_class = with || ::ActionPolicy.lookup(
13
15
  record,
14
- **{namespace, context, allow_nil, default}
16
+ namespace:, context:, allow_nil:, default:, strict_namespace:
15
17
  )
16
18
  policy_class&.new(record, **context)
17
19
  end
18
20
 
19
21
  def authorization_context
20
- raise NotImplementedError, "Please, define `authorization_context` method!"
22
+ Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
23
  end
22
24
 
23
25
  def authorization_namespace
@@ -28,6 +30,10 @@ module ActionPolicy
28
30
  # override to provide a policy class use when no policy found
29
31
  end
30
32
 
33
+ def authorization_strict_namespace
34
+ # override to provide strict namespace lookup option
35
+ end
36
+
31
37
  # Override this method to provide implicit authorization target
32
38
  # that would be used in case `record` is not specified in
33
39
  # `authorize!` and `allowed_to?` call.
@@ -39,7 +45,7 @@ module ActionPolicy
39
45
 
40
46
  # Return implicit authorization target or raises an exception if it's nil
41
47
  def implicit_authorization_target!
42
- implicit_authorization_target || raise(
48
+ implicit_authorization_target || Kernel.raise(
43
49
  NotFound,
44
50
  [
45
51
  self,
@@ -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:
@@ -52,7 +52,7 @@ module ActionPolicy
52
52
  class << self
53
53
  attr_accessor :chain, :namespace_cache_enabled
54
54
 
55
- alias namespace_cache_enabled? namespace_cache_enabled
55
+ alias_method :namespace_cache_enabled?, :namespace_cache_enabled
56
56
 
57
57
  def call(record, **opts)
58
58
  chain.each do |probe|
@@ -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
@@ -126,7 +133,7 @@ module ActionPolicy
126
133
  end
127
134
 
128
135
  # An alias for readability purposes
129
- def check?(*args) = allowed_to?(*args)
136
+ def check?(*args, **hargs) = allowed_to?(*args, **hargs)
130
137
 
131
138
  # Returns a rule name (policy method name) for activity.
132
139
  #
@@ -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
@@ -31,6 +31,20 @@ module ActionPolicy
31
31
 
32
32
  def present?() = !empty?
33
33
 
34
+ def merge(other)
35
+ other.reasons.each do |policy_class, rules|
36
+ reasons[policy_class] ||= []
37
+
38
+ rules.each do |rule|
39
+ if rule.is_a?(::Hash)
40
+ add_detailed_reason(reasons[policy_class], rule)
41
+ else
42
+ add_non_detailed_reason(reasons[policy_class], rule)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
34
48
  private
35
49
 
36
50
  def add_non_detailed_reason(store, rule)
@@ -72,7 +86,7 @@ module ActionPolicy
72
86
  def all_details
73
87
  return @all_details if defined?(@all_details)
74
88
 
75
- {}.tap do |all|
89
+ @all_details = {}.tap do |all|
76
90
  next unless defined?(@reasons)
77
91
 
78
92
  reasons.reasons.each_value do |rules|
@@ -84,7 +98,7 @@ module ActionPolicy
84
98
  all.merge!(details)
85
99
  end
86
100
  end
87
- end => @all_details
101
+ end
88
102
  end
89
103
 
90
104
  # Add reasons to inspect
@@ -182,7 +196,7 @@ module ActionPolicy
182
196
  result.details ||= {}
183
197
  end
184
198
 
185
- def allowed_to?(rule, record = :__undef__, **options)
199
+ def allowed_to?(rule, record = :__undef__, inline_reasons: false, **options)
186
200
  res =
187
201
  if (record == :__undef__ || record == self.record) && options.empty?
188
202
  rule = resolve_rule(rule)
@@ -196,7 +210,9 @@ module ActionPolicy
196
210
  policy.result
197
211
  end
198
212
 
199
- result&.reasons&.add(policy, rule, res.details) if res.fail?
213
+ if res.fail? && result&.reasons
214
+ inline_reasons ? result.reasons.merge(res.reasons) : result.reasons.add(policy, rule, res.details)
215
+ end
200
216
 
201
217
  res.clear_details
202
218
 
@@ -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
@@ -57,7 +57,7 @@ module ActionPolicy
57
57
  end
58
58
 
59
59
  def verify_authorized
60
- raise UnauthorizedAction.new(controller_path, action_name) if
60
+ Kernel.raise UnauthorizedAction.new(controller_path, action_name) if
61
61
  authorize_count.zero? && !verify_authorized_skipped
62
62
  end
63
63
 
@@ -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.4"
4
+ VERSION = "0.6.0"
5
5
  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) ||
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-09 00:00:00.000000000 Z
11
+ date: 2021-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -16,14 +16,14 @@ dependencies:
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
@@ -133,6 +133,10 @@ files:
133
133
  - LICENSE.txt
134
134
  - README.md
135
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
136
140
  - lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
137
141
  - lib/.rbnext/2.7/action_policy/i18n.rb
138
142
  - lib/.rbnext/2.7/action_policy/policy/cache.rb
@@ -140,20 +144,15 @@ files:
140
144
  - lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
141
145
  - lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
142
146
  - lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
143
- - lib/.rbnext/3.0/action_policy/behaviour.rb
144
- - lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
145
- - lib/.rbnext/3.0/action_policy/behaviours/scoping.rb
146
147
  - lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
147
148
  - lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
148
149
  - lib/.rbnext/3.0/action_policy/policy/aliases.rb
149
- - lib/.rbnext/3.0/action_policy/policy/authorization.rb
150
150
  - lib/.rbnext/3.0/action_policy/policy/cache.rb
151
151
  - lib/.rbnext/3.0/action_policy/policy/core.rb
152
152
  - lib/.rbnext/3.0/action_policy/policy/defaults.rb
153
153
  - lib/.rbnext/3.0/action_policy/policy/execution_result.rb
154
154
  - lib/.rbnext/3.0/action_policy/policy/pre_check.rb
155
155
  - lib/.rbnext/3.0/action_policy/policy/reasons.rb
156
- - lib/.rbnext/3.0/action_policy/policy/scoping.rb
157
156
  - lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb
158
157
  - lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb
159
158
  - lib/.rbnext/3.0/action_policy/utils/pretty_print.rb
@@ -223,7 +222,7 @@ metadata:
223
222
  documentation_uri: https://actionpolicy.evilmartians.io/
224
223
  homepage_uri: https://actionpolicy.evilmartians.io/
225
224
  source_code_uri: http://github.com/palkan/action_policy
226
- post_install_message:
225
+ post_install_message:
227
226
  rdoc_options: []
228
227
  require_paths:
229
228
  - lib
@@ -231,15 +230,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
231
230
  requirements:
232
231
  - - ">="
233
232
  - !ruby/object:Gem::Version
234
- version: 2.5.0
233
+ version: 2.6.0
235
234
  required_rubygems_version: !ruby/object:Gem::Requirement
236
235
  requirements:
237
236
  - - ">="
238
237
  - !ruby/object:Gem::Version
239
238
  version: '0'
240
239
  requirements: []
241
- rubygems_version: 3.0.6
242
- signing_key:
240
+ rubygems_version: 3.2.15
241
+ signing_key:
243
242
  specification_version: 4
244
243
  summary: Authorization framework for Ruby/Rails application
245
244
  test_files: []
@@ -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