action_policy 0.3.4 → 0.4.0

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