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 +4 -4
- data/.travis.yml +2 -2
- data/CHANGELOG.md +21 -0
- data/README.md +5 -3
- data/docs/instrumentation.md +11 -0
- data/docs/quick_start.md +4 -2
- data/docs/rails.md +7 -0
- data/docs/testing.md +6 -0
- data/docs/writing_policies.md +1 -1
- data/lib/action_policy.rb +1 -1
- data/lib/action_policy/behaviour.rb +4 -0
- data/lib/action_policy/behaviours/memoized.rb +3 -7
- data/lib/action_policy/behaviours/policy_for.rb +13 -4
- data/lib/action_policy/behaviours/scoping.rb +2 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +3 -7
- data/lib/action_policy/lookup_chain.rb +4 -4
- data/lib/action_policy/policy/core.rb +2 -2
- data/lib/action_policy/policy/pre_check.rb +2 -2
- data/lib/action_policy/policy/reasons.rb +1 -1
- data/lib/action_policy/rails/policy/instrumentation.rb +9 -2
- data/lib/action_policy/rspec/dsl.rb +4 -4
- data/lib/action_policy/rspec/have_authorized_scope.rb +2 -2
- data/lib/action_policy/testing.rb +3 -3
- data/lib/action_policy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4dadd0bd9a76357e15ca389c572fcf3bdc3f78c56482652d364a08015e4b5c7a
|
4
|
+
data.tar.gz: e38e115067ad978de53354ad29fd378e04871fd05b9853c2123c8d2514ea5f04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2036718d582c6a55fe8ad1242b9971c1563e9a83673f7ae608f818c2930409ac32bb43517e28847916a02c3aaa75f927c8bf9be05b8893e0ceb163e5dbbf4c1e
|
7
|
+
data.tar.gz: c90eb2088ce0ea1c1b6271f5c8fa03757f720294521f7ad6c70224ac79bb0a474e6b1a57c6d2f0f458cc3073085a052ed81dde61ece1ca797fc21a3e118b786a
|
data/.travis.yml
CHANGED
@@ -16,7 +16,7 @@ matrix:
|
|
16
16
|
include:
|
17
17
|
- rvm: ruby-head
|
18
18
|
gemfile: gemfiles/railsmaster.gemfile
|
19
|
-
- rvm: jruby-9.2.
|
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.
|
30
|
+
- rvm: jruby-9.2.8.0
|
31
31
|
gemfile: gemfiles/jruby.gemfile
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
-
|
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
|
-
-
|
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.
|
34
|
+
gem "action_policy", "~> 0.4.0"
|
33
35
|
```
|
34
36
|
|
35
37
|
And then execute:
|
data/docs/instrumentation.md
CHANGED
@@ -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:
|
data/docs/quick_start.md
CHANGED
@@ -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
|
-
|
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
|
|
data/docs/rails.md
CHANGED
@@ -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`).
|
data/docs/testing.md
CHANGED
@@ -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.
|
data/docs/writing_policies.md
CHANGED
@@ -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?)
|
data/lib/action_policy.rb
CHANGED
@@ -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,
|
40
|
-
|
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:
|
9
|
-
namespace
|
10
|
-
policy_class
|
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,
|
58
|
-
|
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,
|
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!
|
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
|
-
|
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(
|
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(&
|
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(&
|
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
|
@@ -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
|
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
|
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.
|
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
|
+
date: 2019-12-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ammeter
|