action_policy 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -171
  3. data/LICENSE.txt +1 -1
  4. data/README.md +7 -11
  5. data/lib/action_policy.rb +7 -1
  6. data/lib/action_policy/behaviour.rb +22 -16
  7. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  8. data/lib/action_policy/behaviours/scoping.rb +2 -1
  9. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  10. data/lib/action_policy/ext/module_namespace.rb +1 -6
  11. data/lib/action_policy/ext/policy_cache_key.rb +15 -33
  12. data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
  13. data/lib/action_policy/i18n.rb +1 -1
  14. data/lib/action_policy/lookup_chain.rb +41 -21
  15. data/lib/action_policy/policy/aliases.rb +7 -12
  16. data/lib/action_policy/policy/authorization.rb +14 -17
  17. data/lib/action_policy/policy/cache.rb +34 -18
  18. data/lib/action_policy/policy/core.rb +25 -12
  19. data/lib/action_policy/policy/defaults.rb +3 -9
  20. data/lib/action_policy/policy/execution_result.rb +3 -9
  21. data/lib/action_policy/policy/pre_check.rb +19 -58
  22. data/lib/action_policy/policy/reasons.rb +30 -20
  23. data/lib/action_policy/policy/scoping.rb +5 -6
  24. data/lib/action_policy/rails/controller.rb +6 -1
  25. data/lib/action_policy/rails/ext/active_record.rb +7 -0
  26. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  27. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  28. data/lib/action_policy/rspec/dsl.rb +3 -3
  29. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  30. data/lib/action_policy/testing.rb +1 -1
  31. data/lib/action_policy/utils/pretty_print.rb +21 -24
  32. data/lib/action_policy/utils/suggest_message.rb +1 -3
  33. data/lib/action_policy/version.rb +1 -1
  34. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
  35. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  36. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  37. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  38. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  39. metadata +30 -119
  40. data/.gitattributes +0 -2
  41. data/.github/FUNDING.yml +0 -1
  42. data/.github/ISSUE_TEMPLATE.md +0 -18
  43. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  44. data/.gitignore +0 -15
  45. data/.rubocop.yml +0 -54
  46. data/.tidelift.yml +0 -6
  47. data/.travis.yml +0 -31
  48. data/Gemfile +0 -22
  49. data/Rakefile +0 -27
  50. data/action_policy.gemspec +0 -44
  51. data/benchmarks/namespaced_lookup_cache.rb +0 -71
  52. data/bin/console +0 -14
  53. data/bin/setup +0 -8
  54. data/docs/.nojekyll +0 -0
  55. data/docs/CNAME +0 -1
  56. data/docs/README.md +0 -77
  57. data/docs/_sidebar.md +0 -27
  58. data/docs/aliases.md +0 -122
  59. data/docs/assets/docsify-search.js +0 -364
  60. data/docs/assets/docsify.min.js +0 -3
  61. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  62. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  63. data/docs/assets/images/banner.png +0 -0
  64. data/docs/assets/images/cache.png +0 -0
  65. data/docs/assets/images/cache.svg +0 -70
  66. data/docs/assets/images/layer.png +0 -0
  67. data/docs/assets/images/layer.svg +0 -35
  68. data/docs/assets/prism-ruby.min.js +0 -1
  69. data/docs/assets/styles.css +0 -347
  70. data/docs/assets/vue.min.css +0 -1
  71. data/docs/authorization_context.md +0 -92
  72. data/docs/behaviour.md +0 -113
  73. data/docs/caching.md +0 -273
  74. data/docs/controller_action_aliases.md +0 -109
  75. data/docs/custom_lookup_chain.md +0 -48
  76. data/docs/custom_policy.md +0 -53
  77. data/docs/debugging.md +0 -55
  78. data/docs/decorators.md +0 -27
  79. data/docs/favicon.ico +0 -0
  80. data/docs/graphql.md +0 -302
  81. data/docs/i18n.md +0 -44
  82. data/docs/index.html +0 -43
  83. data/docs/instrumentation.md +0 -84
  84. data/docs/lookup_chain.md +0 -17
  85. data/docs/namespaces.md +0 -77
  86. data/docs/non_rails.md +0 -28
  87. data/docs/pre_checks.md +0 -57
  88. data/docs/pundit_migration.md +0 -80
  89. data/docs/quick_start.md +0 -118
  90. data/docs/rails.md +0 -120
  91. data/docs/reasons.md +0 -120
  92. data/docs/scoping.md +0 -255
  93. data/docs/testing.md +0 -333
  94. data/docs/writing_policies.md +0 -107
  95. data/gemfiles/jruby.gemfile +0 -8
  96. data/gemfiles/rails42.gemfile +0 -8
  97. data/gemfiles/rails6.gemfile +0 -8
  98. data/gemfiles/railsmaster.gemfile +0 -6
  99. data/lib/action_policy/ext/string_match.rb +0 -14
  100. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -46,23 +46,18 @@ module ActionPolicy
46
46
  end
47
47
  end
48
48
 
49
- def lookup_alias(rule)
50
- rules_aliases[rule]
51
- end
49
+ def lookup_alias(rule) = rules_aliases[rule]
52
50
 
53
- def lookup_default_rule
54
- rules_aliases[DEFAULT]
55
- end
51
+ def lookup_default_rule() = rules_aliases[DEFAULT]
56
52
 
57
53
  def rules_aliases
58
54
  return @rules_aliases if instance_variable_defined?(:@rules_aliases)
59
55
 
60
- @rules_aliases =
61
- if superclass.respond_to?(:rules_aliases)
62
- superclass.rules_aliases.dup
63
- else
64
- {}
65
- end
56
+ if superclass.respond_to?(:rules_aliases)
57
+ superclass.rules_aliases.dup
58
+ else
59
+ {}
60
+ end => @rules_aliases
66
61
  end
67
62
 
68
63
  def method_added(name)
@@ -43,21 +43,17 @@ module ActionPolicy
43
43
 
44
44
  attr_reader :authorization_context
45
45
 
46
- def initialize(*args, **params)
47
- super(*args)
46
+ def initialize(record = nil, **params)
47
+ super(record)
48
48
 
49
49
  @authorization_context = {}
50
50
 
51
51
  self.class.authorization_targets.each do |id, opts|
52
- if opts[:optional] == true
53
- val = params.fetch(id, nil)
54
- else
55
- raise AuthorizationContextMissing, id unless params.key?(id)
52
+ raise AuthorizationContextMissing, id unless params.key?(id) || opts[:optional]
56
53
 
57
- val = params.fetch(id)
54
+ val = params.fetch(id, nil)
58
55
 
59
- raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
60
- end
56
+ raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
61
57
 
62
58
  authorization_context[id] = instance_variable_set("@#{id}", val)
63
59
  end
@@ -66,9 +62,11 @@ module ActionPolicy
66
62
  end
67
63
 
68
64
  module ClassMethods # :nodoc:
69
- def authorize(*ids, **opts)
65
+ def authorize(*ids, allow_nil: false, optional: false)
66
+ allow_nil ||= optional
67
+
70
68
  ids.each do |id|
71
- authorization_targets[id] = opts
69
+ authorization_targets[id] = {allow_nil, optional}
72
70
  end
73
71
 
74
72
  attr_reader(*ids)
@@ -77,12 +75,11 @@ module ActionPolicy
77
75
  def authorization_targets
78
76
  return @authorization_targets if instance_variable_defined?(:@authorization_targets)
79
77
 
80
- @authorization_targets =
81
- if superclass.respond_to?(:authorization_targets)
82
- superclass.authorization_targets.dup
83
- else
84
- {}
85
- end
78
+ if superclass.respond_to?(:authorization_targets)
79
+ superclass.authorization_targets.dup
80
+ else
81
+ {}
82
+ end => @authorization_targets
86
83
  end
87
84
  end
88
85
  end
@@ -3,16 +3,13 @@
3
3
  require "action_policy/version"
4
4
 
5
5
  module ActionPolicy # :nodoc:
6
+ using RubyNext
7
+
6
8
  # By default cache namespace (or prefix) contains major and minor version of the gem
7
9
  CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(".").take(2).join(".")}"
8
10
 
9
- require "action_policy/ext/yield_self_then"
10
11
  require "action_policy/ext/policy_cache_key"
11
12
 
12
- unless "".respond_to?(:then)
13
- require "action_policy/ext/yield_self_then"
14
- using ActionPolicy::Ext::YieldSelfThen
15
- end
16
13
  using ActionPolicy::Ext::PolicyCacheKey
17
14
 
18
15
  module Policy
@@ -26,22 +23,31 @@ module ActionPolicy # :nodoc:
26
23
  end
27
24
  end
28
25
 
29
- def cache_namespace
30
- ActionPolicy::CACHE_NAMESPACE
26
+ def cache_namespace() = ActionPolicy::CACHE_NAMESPACE
27
+
28
+ def cache_key(*parts)
29
+ [
30
+ cache_namespace,
31
+ *parts
32
+ ].map { _1._policy_cache_key }.join("/")
31
33
  end
32
34
 
33
- def cache_key(rule)
34
- "#{cache_namespace}/#{context_cache_key}/" \
35
- "#{record._policy_cache_key}/#{self.class.name}/#{rule}"
35
+ def rule_cache_key(rule)
36
+ cache_key(
37
+ context_cache_key,
38
+ record,
39
+ self.class,
40
+ rule
41
+ )
36
42
  end
37
43
 
38
44
  def context_cache_key
39
- authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
45
+ authorization_context.map { _2._policy_cache_key.to_s }.join("/")
40
46
  end
41
47
 
42
48
  def apply_with_cache(rule)
43
49
  options = self.class.cached_rules.fetch(rule)
44
- key = cache_key(rule)
50
+ key = rule_cache_key(rule)
45
51
 
46
52
  ActionPolicy.cache_store.then do |store|
47
53
  @result = store.read(key)
@@ -62,6 +68,17 @@ module ActionPolicy # :nodoc:
62
68
  apply_with_cache(rule) { super }
63
69
  end
64
70
 
71
+ def cache(*parts, **options)
72
+ key = cache_key(*parts)
73
+ ActionPolicy.cache_store.then do |store|
74
+ res = store.read(key)
75
+ next res unless res.nil?
76
+ res = yield
77
+ store.write(key, res, options)
78
+ res
79
+ end
80
+ end
81
+
65
82
  module ClassMethods # :nodoc:
66
83
  def cache(*rules, **options)
67
84
  rules.each do |rule|
@@ -72,12 +89,11 @@ module ActionPolicy # :nodoc:
72
89
  def cached_rules
73
90
  return @cached_rules if instance_variable_defined?(:@cached_rules)
74
91
 
75
- @cached_rules =
76
- if superclass.respond_to?(:cached_rules)
77
- superclass.cached_rules.dup
78
- else
79
- {}
80
- end
92
+ if superclass.respond_to?(:cached_rules)
93
+ superclass.cached_rules.dup
94
+ else
95
+ {}
96
+ end => @cached_rules
81
97
  end
82
98
  end
83
99
  end
@@ -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