action_policy 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 272123a33fae55ffd9e6e09b1098862fbbf353a6ed5ff71d090bc5fd01396244
4
- data.tar.gz: ec53e2634999660b46c95dddc85bcf71055e336fdae5a8ea47023a671bba55ce
3
+ metadata.gz: 4dadd0bd9a76357e15ca389c572fcf3bdc3f78c56482652d364a08015e4b5c7a
4
+ data.tar.gz: e38e115067ad978de53354ad29fd378e04871fd05b9853c2123c8d2514ea5f04
5
5
  SHA512:
6
- metadata.gz: 62353ff2be45386257efcd127a43c7eac61d1cb7fc3cfcbf1acc6d6563e6d0a0a91873a0593b8ff62d51adc70b730e1799eb5a1565e3a1ed3c1f2593d46a05ff
7
- data.tar.gz: 837e717de9323e7131c535a41fe848aaa9501ad8f10471f082ff1355ae824afa7691b44bb9c8db71cb0b33898ad126963f3518b8cfcc1207e34469a282648d62
6
+ metadata.gz: 2036718d582c6a55fe8ad1242b9971c1563e9a83673f7ae608f818c2930409ac32bb43517e28847916a02c3aaa75f927c8bf9be05b8893e0ceb163e5dbbf4c1e
7
+ data.tar.gz: c90eb2088ce0ea1c1b6271f5c8fa03757f720294521f7ad6c70224ac79bb0a474e6b1a57c6d2f0f458cc3073085a052ed81dde61ece1ca797fc21a3e118b786a
@@ -16,7 +16,7 @@ matrix:
16
16
  include:
17
17
  - rvm: ruby-head
18
18
  gemfile: gemfiles/railsmaster.gemfile
19
- - rvm: jruby-9.2.5.0
19
+ - rvm: jruby-9.2.8.0
20
20
  gemfile: gemfiles/jruby.gemfile
21
21
  - rvm: 2.6.0
22
22
  gemfile: gemfiles/rails6.gemfile
@@ -27,5 +27,5 @@ matrix:
27
27
  allow_failures:
28
28
  - rvm: ruby-head
29
29
  gemfile: gemfiles/railsmaster.gemfile
30
- - rvm: jruby-9.2.5.0
30
+ - rvm: jruby-9.2.8.0
31
31
  gemfile: gemfiles/jruby.gemfile
@@ -1,5 +1,26 @@
1
+ # Change log
2
+
1
3
  ## master
2
4
 
5
+ ## 0.4.0 (2019-12-11)
6
+
7
+ - Add `action_policy.init` instrumentation event. ([@palkan][])
8
+
9
+ Triggered every time a new policy object is initialized.
10
+
11
+ - Fix policy memoization with explicit context. ([@palkan][])
12
+
13
+ Explicit context (`authorize! context: {}`) wasn't considered during
14
+ policies memoization. Not this is fixed.
15
+
16
+ - Support composed matchers for authorization target testing. ([@palkan][])
17
+
18
+ Now you can write tests like this:
19
+
20
+ ```ruby
21
+ expect { subject }.to be_authorized_to(:show?, an_instance_of(User))
22
+ ```
23
+
3
24
  ## 0.3.4 (2019-11-27)
4
25
 
5
26
  - Fix Rails generators. ([@palkan][])
data/README.md CHANGED
@@ -15,9 +15,11 @@ Composable. Extensible. Performant.
15
15
 
16
16
  ## Resources
17
17
 
18
- - Seattle.rb, 2019 "A Denial!" talk [[slides](https://speakerdeck.com/palkan/seattle-dot-rb-2019-a-denial)]
18
+ - RubyRussia, 2019 "Welcome, or access denied?" talk ([video](https://www.youtube.com/watch?v=y15a2g7v8i0) [RU], [slides](https://speakerdeck.com/palkan/rubyrussia-2019-welcome-or-access-denied))
19
19
 
20
- - RailsConf, 2018 "Access Denied" talk [[video](https://www.youtube.com/watch?v=NVwx0DARDis), [slides](https://speakerdeck.com/palkan/railsconf-2018-access-denied-the-missing-guide-to-authorization-in-rails)]
20
+ - Seattle.rb, 2019 "A Denial!" talk ([slides](https://speakerdeck.com/palkan/seattle-dot-rb-2019-a-denial))
21
+
22
+ - RailsConf, 2018 "Access Denied" talk ([video](https://www.youtube.com/watch?v=NVwx0DARDis), [slides](https://speakerdeck.com/palkan/railsconf-2018-access-denied-the-missing-guide-to-authorization-in-rails))
21
23
 
22
24
 
23
25
  ## Integrations
@@ -29,7 +31,7 @@ Composable. Extensible. Performant.
29
31
  Add this line to your application's `Gemfile`:
30
32
 
31
33
  ```ruby
32
- gem "action_policy", "~> 0.3.0"
34
+ gem "action_policy", "~> 0.4.0"
33
35
  ```
34
36
 
35
37
  And then execute:
@@ -47,6 +47,17 @@ permitted to do that).
47
47
 
48
48
  The `action_policy.apply_rule` might have a large number of failures, 'cause it also tracks the usage of non-raising applications (i.e. `allowed_to?`).
49
49
 
50
+ ### `action_policy.init`
51
+
52
+ This event is triggered every time a new policy object is initialized.
53
+
54
+ The event contains the following information:
55
+
56
+ - `:policy` – policy class name.
57
+
58
+ This event is useful if you want to track the number of initialized policies per _action_ (for example, when you want to ensure that
59
+ the [memoization](caching.md) works as expected).
60
+
50
61
  ## Turn off instrumentation
51
62
 
52
63
  Instrumentation is enabled by default. To turn it off add to your configuration:
@@ -34,9 +34,11 @@ class ApplicationPolicy < ActionPolicy::Base
34
34
  end
35
35
  ```
36
36
 
37
- You could use the following command to generate it.
37
+ You could use the following command to generate it when using Rails:
38
38
 
39
- $ rails generate action_policy:install
39
+ ```sh
40
+ rails generate action_policy:install
41
+ ```
40
42
 
41
43
  **NOTE:** it is not necessary to inherit from `ActionPolicy::Base`; instead, you can [construct basic policy](custom_policy.md) choosing only the components you need.
42
44
 
@@ -6,6 +6,13 @@ In most cases, you do not have to do anything except writing policy files and ad
6
6
 
7
7
  **NOTE:** both controllers and channels extensions are built on top of the Action Policy [behaviour](./behaviour.md) mixin.
8
8
 
9
+ ## Generators
10
+
11
+ Action Policy provides a couple of useful Rails generators:
12
+
13
+ - `rails g action_policy:install` — adds `app/policies/application_policy.rb` file
14
+ - `rails g action_policy:policy MODEL_NAME` — adds a policy file and a policy test file for a given model (also creates an `application_policy.rb` if it's missing)
15
+
9
16
  ## Controllers integration
10
17
 
11
18
  Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
@@ -230,6 +230,12 @@ end
230
230
 
231
231
  If you omit `.with(PostPolicy)` then the inferred policy for the target (`post`) would be used.
232
232
 
233
+ RSpec composed matchers are available as target:
234
+
235
+ ```ruby
236
+ expect { subject }.to be_authorized_to(:show?, an_instance_of(Post))
237
+ ```
238
+
233
239
  ## Testing scoping
234
240
 
235
241
  Action Policy provides a way to test that a correct scoping has been applied during the code execution.
@@ -56,7 +56,7 @@ You can also specify all the usual options (such as `with`).
56
56
 
57
57
  There is also a `check?` method which is just an "alias"\* for `allowed_to?` added for better readability:
58
58
 
59
- ```
59
+ ```ruby
60
60
  class PostPolicy < ApplicationPolicy
61
61
  def show?
62
62
  user.admin? || check?(:publicly_visible?)
@@ -31,7 +31,7 @@ module ActionPolicy
31
31
 
32
32
  # Find a policy class for a target
33
33
  def lookup(target, allow_nil: false, **options)
34
- LookupChain.call(target, options) ||
34
+ LookupChain.call(target, **options) ||
35
35
  (allow_nil ? nil : raise(NotFound, target))
36
36
  end
37
37
  end
@@ -38,6 +38,8 @@ module ActionPolicy
38
38
  record = implicit_authorization_target! if record == :__undef__
39
39
  raise ArgumentError, "Record must be specified" if record.nil?
40
40
 
41
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
42
+
41
43
  policy = policy_for(record: record, **options)
42
44
 
43
45
  Authorizer.call(policy, authorization_rule_for(policy, to))
@@ -50,6 +52,8 @@ module ActionPolicy
50
52
  record = implicit_authorization_target! if record == :__undef__
51
53
  raise ArgumentError, "Record must be specified" if record.nil?
52
54
 
55
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
56
+
53
57
  policy = policy_for(record: record, **options)
54
58
 
55
59
  policy.apply(authorization_rule_for(policy, rule))
@@ -19,9 +19,6 @@ module ActionPolicy
19
19
  #
20
20
  # policy.equal?(policy_for(record, with: CustomPolicy)) #=> false
21
21
  module Memoized
22
- require "action_policy/ext/policy_cache_key"
23
- using ActionPolicy::Ext::PolicyCacheKey
24
-
25
22
  class << self
26
23
  def prepended(base)
27
24
  base.prepend InstanceMethods
@@ -32,13 +29,12 @@ module ActionPolicy
32
29
 
33
30
  module InstanceMethods # :nodoc:
34
31
  def policy_for(record:, **opts)
35
- __policy_memoize__(record, opts) { super(record: record, **opts) }
32
+ __policy_memoize__(record, **opts) { super(record: record, **opts) }
36
33
  end
37
34
  end
38
35
 
39
- def __policy_memoize__(record, with: nil, namespace: nil, **_opts)
40
- record_key = record._policy_cache_key(use_object_id: true)
41
- cache_key = "#{namespace}/#{with}/#{record_key}"
36
+ def __policy_memoize__(record, **options)
37
+ cache_key = policy_for_cache_key(record: record, **options)
42
38
 
43
39
  return __policies_cache__[cache_key] if
44
40
  __policies_cache__.key?(cache_key)
@@ -4,11 +4,13 @@ module ActionPolicy
4
4
  module Behaviours
5
5
  # Adds `policy_for` method
6
6
  module PolicyFor
7
+ require "action_policy/ext/policy_cache_key"
8
+ using ActionPolicy::Ext::PolicyCacheKey
9
+
7
10
  # Returns policy instance for the record.
8
- def policy_for(record:, with: nil, namespace: nil, context: nil, **options)
9
- namespace ||= authorization_namespace
10
- policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace, **options)
11
- policy_class&.new(record, authorization_context.tap { |ctx| ctx.merge!(context) if context })
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: authorization_context, allow_nil: false)
12
+ policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace, context: context, allow_nil: allow_nil)
13
+ policy_class&.new(record, **context)
12
14
  end
13
15
 
14
16
  def authorization_context
@@ -41,6 +43,13 @@ module ActionPolicy
41
43
  ]
42
44
  )
43
45
  end
46
+
47
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
48
+ record_key = record._policy_cache_key(use_object_id: true)
49
+ context_key = context.values.map { |v| v._policy_cache_key(use_object_id: true) }.join(".")
50
+
51
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
52
+ end
44
53
  end
45
54
  end
46
55
  end
@@ -11,6 +11,8 @@ module ActionPolicy
11
11
  # - secondly, try to infer policy class from `target` (non-raising lookup)
12
12
  # - use `implicit_authorization_target` if none of the above works.
13
13
  def authorized_scope(target, type: nil, as: :default, scope_options: nil, **options)
14
+ options[:context] && (options[:context] = authorization_context.merge(options[:context]))
15
+
14
16
  policy = policy_for(record: target, allow_nil: true, **options)
15
17
  policy ||= policy_for(record: implicit_authorization_target!, **options)
16
18
 
@@ -37,9 +37,6 @@ module ActionPolicy
37
37
  #
38
38
  # NOTE: don't forget to clear thread cache with ActionPolicy::PerThreadCache.clear_all
39
39
  module ThreadMemoized
40
- require "action_policy/ext/policy_cache_key"
41
- using ActionPolicy::Ext::PolicyCacheKey
42
-
43
40
  class << self
44
41
  def prepended(base)
45
42
  base.prepend InstanceMethods
@@ -50,13 +47,12 @@ module ActionPolicy
50
47
 
51
48
  module InstanceMethods # :nodoc:
52
49
  def policy_for(record:, **opts)
53
- __policy_thread_memoize__(record, opts) { super(record: record, **opts) }
50
+ __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
54
51
  end
55
52
  end
56
53
 
57
- def __policy_thread_memoize__(record, with: nil, namespace: nil, **_opts)
58
- record_key = record._policy_cache_key(use_object_id: true)
59
- cache_key = "#{namespace}/#{with}/#{record_key}"
54
+ def __policy_thread_memoize__(record, **options)
55
+ cache_key = policy_for_cache_key(record: record, **options)
60
56
 
61
57
  ActionPolicy::PerThreadCache.fetch(cache_key) { yield }
62
58
  end
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  def call(record, **opts)
47
47
  chain.each do |probe|
48
- val = probe.call(record, opts)
48
+ val = probe.call(record, **opts)
49
49
  return val unless val.nil?
50
50
  end
51
51
  nil
@@ -88,12 +88,12 @@ module ActionPolicy
88
88
  !ENV["RACK_ENV"].nil? ? ENV["RACK_ENV"] == "production" : true
89
89
 
90
90
  # By self `policy_class` method
91
- INSTANCE_POLICY_CLASS = ->(record, _) {
91
+ INSTANCE_POLICY_CLASS = ->(record, **) {
92
92
  record.policy_class if record.respond_to?(:policy_class)
93
93
  }
94
94
 
95
95
  # By record's class `policy_class` method
96
- CLASS_POLICY_CLASS = ->(record, _) {
96
+ CLASS_POLICY_CLASS = ->(record, **) {
97
97
  record.class.policy_class if record.class.respond_to?(:policy_class)
98
98
  }
99
99
 
@@ -106,7 +106,7 @@ module ActionPolicy
106
106
  }
107
107
 
108
108
  # Infer from class name
109
- INFER_FROM_CLASS = ->(record, _) {
109
+ INFER_FROM_CLASS = ->(record, **) {
110
110
  policy_class_name_for(record).safe_constantize
111
111
  }
112
112
 
@@ -66,7 +66,7 @@ module ActionPolicy
66
66
  attr_reader :record, :result
67
67
 
68
68
  # NEXT_RELEASE: deprecate `record` arg, migrate to `record: nil`
69
- def initialize(record = nil, _opts = nil)
69
+ def initialize(record = nil, *)
70
70
  @record = record
71
71
  end
72
72
 
@@ -101,7 +101,7 @@ module ActionPolicy
101
101
  #
102
102
  # If record is `nil` then we uses the current policy.
103
103
  def allowed_to?(rule, record = :__undef__, **options)
104
- if record == :__undef__ && options.empty?
104
+ if (record == :__undef__ || record == self.record) && options.empty?
105
105
  __apply__(rule)
106
106
  else
107
107
  policy_for(record: record, **options).apply(rule)
@@ -168,7 +168,7 @@ module ActionPolicy
168
168
  check = pre_checks.find { |c| c.name == name }
169
169
  raise "Pre-check already defined: #{name}" unless check.nil?
170
170
 
171
- pre_checks << Check.new(self, name, options)
171
+ pre_checks << Check.new(self, name, **options)
172
172
  end
173
173
  end
174
174
 
@@ -181,7 +181,7 @@ module ActionPolicy
181
181
  next pre_checks.delete(check) if options.empty?
182
182
 
183
183
  # otherwise duplicate and apply skip options
184
- pre_checks[pre_checks.index(check)] = check.dup.tap { |c| c.skip! options }
184
+ pre_checks[pre_checks.index(check)] = check.dup.tap { |c| c.skip!(**options) }
185
185
  end
186
186
  end
187
187
 
@@ -179,7 +179,7 @@ module ActionPolicy
179
179
 
180
180
  def allowed_to?(rule, record = :__undef__, **options)
181
181
  res =
182
- if record == :__undef__
182
+ if (record == :__undef__ || record == self.record) && options.empty?
183
183
  policy = self
184
184
  with_clean_result { apply(rule) }
185
185
  else
@@ -6,12 +6,19 @@ module ActionPolicy # :nodoc:
6
6
  # Add ActiveSupport::Notifications support.
7
7
  #
8
8
  # Fires `action_policy.apply_rule` event on every `#apply` call.
9
+ # Fires `action_policy.init` event on every policy initialization.
9
10
  module Instrumentation
10
- EVENT_NAME = "action_policy.apply_rule"
11
+ INIT_EVENT_NAME = "action_policy.init"
12
+ APPLY_EVENT_NAME = "action_policy.apply_rule"
13
+
14
+ def initialize(*)
15
+ event = {policy: self.class.name}
16
+ ActiveSupport::Notifications.instrument(INIT_EVENT_NAME, event) { super }
17
+ end
11
18
 
12
19
  def apply(rule)
13
20
  event = {policy: self.class.name, rule: rule.to_s}
14
- ActiveSupport::Notifications.instrument(EVENT_NAME, event) do
21
+ ActiveSupport::Notifications.instrument(APPLY_EVENT_NAME, event) do
15
22
  res = super
16
23
  event[:cached] = result.cached?
17
24
  event[:value] = result.value
@@ -13,18 +13,18 @@ module ActionPolicy
13
13
 
14
14
  ["", "f", "x"].each do |prefix|
15
15
  class_eval <<~CODE, __FILE__, __LINE__ + 1
16
- def #{prefix}succeed(msg = "succeeds", *args, **kwargs)
16
+ def #{prefix}succeed(msg = "succeeds", *args, **kwargs, &block)
17
17
  the_caller = caller
18
18
  #{prefix}context(msg, *args, **kwargs) do
19
- instance_eval(&Proc.new) if block_given?
19
+ instance_eval(&block) if block_given?
20
20
  find_and_eval_shared("examples", "action_policy:policy_rule_example", the_caller.first, true, the_caller)
21
21
  end
22
22
  end
23
23
 
24
- def #{prefix}failed(msg = "fails", *args, **kwargs)
24
+ def #{prefix}failed(msg = "fails", *args, **kwargs, &block)
25
25
  the_caller = caller
26
26
  #{prefix}context(msg, *args, **kwargs) do
27
- instance_eval(&Proc.new) if block_given?
27
+ instance_eval(&block) if block_given?
28
28
  find_and_eval_shared("examples", "action_policy:policy_rule_example", the_caller.first, false, the_caller)
29
29
  end
30
30
  end
@@ -44,8 +44,8 @@ module ActionPolicy
44
44
  self
45
45
  end
46
46
 
47
- def with_target
48
- @target_expectations = Proc.new
47
+ def with_target(&block)
48
+ @target_expectations = block
49
49
  self
50
50
  end
51
51
 
@@ -15,7 +15,7 @@ module ActionPolicy
15
15
 
16
16
  def matches?(policy_class, actual_rule, target)
17
17
  policy_class == policy.class &&
18
- target == policy.record &&
18
+ target === policy.record &&
19
19
  rule == actual_rule
20
20
  end
21
21
 
@@ -107,8 +107,8 @@ module ActionPolicy
107
107
  super
108
108
  end
109
109
 
110
- def scopify(*args)
111
- AuthorizeTracker.track_scope(*args)
110
+ def scopify(*args, **kwargs)
111
+ AuthorizeTracker.track_scope(*args, **kwargs)
112
112
  super
113
113
  end
114
114
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.3.4"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-27 00:00:00.000000000 Z
11
+ date: 2019-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter