action_policy 0.2.4 → 0.3.0.beta1
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 +4 -4
- data/.rubocop.yml +26 -64
- data/.travis.yml +13 -10
- data/CHANGELOG.md +216 -1
- data/Gemfile +7 -0
- data/LICENSE.txt +1 -1
- data/Rakefile +10 -0
- data/action_policy.gemspec +5 -3
- data/benchmarks/namespaced_lookup_cache.rb +18 -22
- data/docs/README.md +3 -3
- data/docs/_sidebar.md +4 -0
- data/docs/aliases.md +9 -5
- data/docs/authorization_context.md +59 -1
- data/docs/behaviour.md +113 -0
- data/docs/caching.md +6 -4
- data/docs/custom_policy.md +1 -2
- data/docs/debugging.md +55 -0
- data/docs/decorators.md +27 -0
- data/docs/i18n.md +41 -2
- data/docs/instrumentation.md +70 -2
- data/docs/lookup_chain.md +5 -4
- data/docs/namespaces.md +1 -1
- data/docs/non_rails.md +2 -3
- data/docs/pundit_migration.md +77 -2
- data/docs/quick_start.md +5 -5
- data/docs/rails.md +5 -2
- data/docs/reasons.md +50 -3
- data/docs/scoping.md +262 -0
- data/docs/testing.md +232 -21
- data/docs/writing_policies.md +1 -1
- data/gemfiles/jruby.gemfile +3 -0
- data/gemfiles/rails42.gemfile +3 -0
- data/gemfiles/rails6.gemfile +8 -0
- data/gemfiles/railsmaster.gemfile +1 -1
- data/lib/action_policy.rb +3 -3
- data/lib/action_policy/authorizer.rb +12 -4
- data/lib/action_policy/base.rb +2 -0
- data/lib/action_policy/behaviour.rb +14 -3
- data/lib/action_policy/behaviours/memoized.rb +1 -1
- data/lib/action_policy/behaviours/policy_for.rb +12 -3
- data/lib/action_policy/behaviours/scoping.rb +32 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
- data/lib/action_policy/ext/hash_transform_keys.rb +19 -0
- data/lib/action_policy/ext/module_namespace.rb +1 -1
- data/lib/action_policy/ext/policy_cache_key.rb +2 -1
- data/lib/action_policy/ext/proc_case_eq.rb +14 -0
- data/lib/action_policy/ext/string_constantize.rb +1 -0
- data/lib/action_policy/ext/symbol_classify.rb +22 -0
- data/lib/action_policy/i18n.rb +56 -0
- data/lib/action_policy/lookup_chain.rb +21 -3
- data/lib/action_policy/policy/cache.rb +10 -6
- data/lib/action_policy/policy/core.rb +31 -19
- data/lib/action_policy/policy/execution_result.rb +12 -0
- data/lib/action_policy/policy/pre_check.rb +2 -6
- data/lib/action_policy/policy/reasons.rb +99 -12
- data/lib/action_policy/policy/scoping.rb +165 -0
- data/lib/action_policy/rails/authorizer.rb +20 -0
- data/lib/action_policy/rails/controller.rb +4 -14
- data/lib/action_policy/rails/ext/active_record.rb +10 -0
- data/lib/action_policy/rails/policy/instrumentation.rb +24 -0
- data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +19 -0
- data/lib/action_policy/rails/scope_matchers/active_record.rb +29 -0
- data/lib/action_policy/railtie.rb +29 -7
- data/lib/action_policy/rspec.rb +1 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +1 -1
- data/lib/action_policy/rspec/dsl.rb +103 -0
- data/lib/action_policy/rspec/have_authorized_scope.rb +126 -0
- data/lib/action_policy/rspec/pundit_syntax.rb +1 -1
- data/lib/action_policy/test_helper.rb +69 -4
- data/lib/action_policy/testing.rb +54 -0
- data/lib/action_policy/utils/pretty_print.rb +137 -0
- data/lib/action_policy/utils/suggest_message.rb +21 -0
- data/lib/action_policy/version.rb +1 -1
- metadata +58 -11
data/docs/i18n.md
CHANGED
@@ -1,5 +1,44 @@
|
|
1
1
|
# I18n Support
|
2
2
|
|
3
|
-
|
3
|
+
`ActionPolicy` integrates with [`i18n`][] to support localizable `full_messages` for [reasons](./reasons.md) and the execution result's `message`:
|
4
4
|
|
5
|
-
|
5
|
+
```ruby
|
6
|
+
class ApplicationController < ActionController::Base
|
7
|
+
rescue_from ActionPolicy::Unauthorized do |ex|
|
8
|
+
p ex.result.message #=> "You do not have access to the stage"
|
9
|
+
p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
The message contains a string for the _rule_ that was called, while `full_messages` contains the list of reasons, why `ActionPolicy::Unauthorized` has been raised. You can find more information about tracking failure reasons [here](./reasons.md).
|
15
|
+
|
16
|
+
## Configuration
|
17
|
+
|
18
|
+
`ActionPolicy` doesn't provide any localization out-of-the-box and uses "You are not authorized to perform this action" as the default message.
|
19
|
+
|
20
|
+
You can add your app-level default fallback by providing the `action_policy.unauthorized` key value.
|
21
|
+
|
22
|
+
When using **Rails**, all you need is to add translations to any file under the `config/locales` folder (or create a new file, e.g. `config/locales/policies.yml`).
|
23
|
+
|
24
|
+
Non-Rails projets should configure [`i18n`][] gem manually:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
I18n.load_path << Dir[File.expand_path("config/locales") + "/*.yml"]
|
28
|
+
```
|
29
|
+
|
30
|
+
## Translations lookup
|
31
|
+
|
32
|
+
`ActionPolicy` uses the `action_policy` scope. Specific policies translations must be stored inside the `policy` sub-scope.
|
33
|
+
|
34
|
+
The following algorithm is used to find out the translation for a policy with a class `klass` and rule `rule`:
|
35
|
+
1. Translation for `"#{klass.identifier}.#{rule}"` key, when `self.identifier =` is not specified then underscored class name without the _Policy_ suffix would be used (e.g. `GuestUserPolicy` turns into `guest_user:` scope)
|
36
|
+
2. Repeat step 1 for each ancestor which looks like a policy (`.respond_to?(:identifier)?`) up to `ActionPolicy::Base`
|
37
|
+
3. Use `#{rule}` key
|
38
|
+
4. Use `en.action_policy.unauthorized` key
|
39
|
+
5. Use a default message provided by the gem
|
40
|
+
|
41
|
+
For example, given a `GuestUserPolicy` class which is inherited from `DefaultUserPolicy` and a rule `feed?`, the following list of possible translation keys would be used: `[:"action_policy.policy.guest_user.feed?", :"action_policy.policy.default_user.feed?", :"action_policy.policy.feed?", :"action_policy.unauthorized"]`
|
42
|
+
|
43
|
+
|
44
|
+
[`i18n`]: https://github.com/svenfuchs/i18n
|
data/docs/instrumentation.md
CHANGED
@@ -1,5 +1,73 @@
|
|
1
1
|
# Instrumentation
|
2
2
|
|
3
|
-
|
3
|
+
Action Policy integrates with [Rails instrumentation system](https://guides.rubyonrails.org/active_support_instrumentation.html), `ActiveSupport::Notifications`.
|
4
4
|
|
5
|
-
|
5
|
+
## Events
|
6
|
+
|
7
|
+
### `action_policy.apply_rule`
|
8
|
+
|
9
|
+
This event is triggered every time a policy rule is applied:
|
10
|
+
- when `authorize!` is called
|
11
|
+
- when `allowed_to?` is called within the policy or the [behaviour](behaviour)
|
12
|
+
- when `apply_rule` is called explicitly (i.e. `SomePolicy.new(record, context).apply_rule(record)`).
|
13
|
+
|
14
|
+
The event contains the following information:
|
15
|
+
- `:policy` – policy class name
|
16
|
+
- `:rule` – applied rule (String)
|
17
|
+
- `:value` – the result of the rule application (true of false)
|
18
|
+
- `:cached` – whether we hit the [cache](caching)\*.
|
19
|
+
|
20
|
+
\* This parameter tracks only the cache store usage, not memoization.
|
21
|
+
|
22
|
+
You can use this event to track your policy cache usage and also detect _slow_ checks.
|
23
|
+
|
24
|
+
Here is an example code for sending policy stats to [Librato](https://librato.com/)
|
25
|
+
using [`librato-rack`](https://github.com/librato/librato-rack):
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
ActiveSupport::Notifications.subscribe("action_policy.apply_rule") do |event, started, finished, _, data|
|
29
|
+
# Track hit and miss events separately (to display two measurements)
|
30
|
+
measurement = "#{event}.#{(data[:cached] ? "hit" : "miss")}"
|
31
|
+
# show ms times
|
32
|
+
timing = ((finished - started) * 1000).to_i
|
33
|
+
Librato.tracker.check_worker
|
34
|
+
Librato.timing measurement, timing, percentile: [95, 99]
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
### `action_policy.authorize`
|
39
|
+
|
40
|
+
This event is identical to `action_policy.apply_rule` with the one difference:
|
41
|
+
**it's only triggered when `authorize!` method is called**.
|
42
|
+
|
43
|
+
The motivation behind having a separate event for this method is to monitor the number of failed
|
44
|
+
authorizations: the high number of failed authorizations usually means that we do not take
|
45
|
+
into account authorization rules in the application UI (e.g., we show a "Delete" button to the user not
|
46
|
+
permitted to do that).
|
47
|
+
|
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
|
+
|
50
|
+
## Turn off instrumentation
|
51
|
+
|
52
|
+
Instrumentation is enabled by default. To turn it off add to your configuration:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
config.action_policy.instrumentation_enabled = false
|
56
|
+
```
|
57
|
+
|
58
|
+
**NOTE:** changing this setting after the application has been initialized doesn't take any effect.
|
59
|
+
|
60
|
+
## Non-Rails usage
|
61
|
+
|
62
|
+
If you don't use Rails itself but have `ActiveSupport::Notifications` available in your application,
|
63
|
+
you can use the instrumentation feature with some additional configuration:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Enable `apply_rule` event by extending the base policy class
|
67
|
+
require "action_policy/rails/policy/instrumentation"
|
68
|
+
ActionPolicy::Base.include ActionPolicy::Policy::Rails::Instrumentation
|
69
|
+
|
70
|
+
# Enabled `authorize` event by extending the authorizer class
|
71
|
+
require "action_policy/rails/authorizer"
|
72
|
+
ActionPolicy::Authorizer.singleton_class.prepend ActionPolicy::Rails::Authorizer
|
73
|
+
```
|
data/docs/lookup_chain.md
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
Action Policy tries to automatically infer policy class from the target using the following _probes_:
|
4
4
|
|
5
|
-
1. If the target
|
6
|
-
2. If the target
|
7
|
-
3. If the target's class responds to `
|
8
|
-
4.
|
5
|
+
1. If the target is a `Symbol`, then use `"#{target.to_s.classify}Policy"` as a `policy_name` (see below);
|
6
|
+
2. If the target responds to `policy_class`, then use it;
|
7
|
+
3. If the target's class responds to `policy_class`, then use it;
|
8
|
+
4. If the target or the target's class responds to `policy_name`, then use it (the `policy_name` should end with `Policy` as it's not appended automatically);
|
9
|
+
5. Otherwise, use `#{target.class.name}Policy`.
|
9
10
|
|
10
11
|
> \* [Namespaces](namespaces.md) could be also be considered when `namespace` option is set.
|
11
12
|
|
data/docs/namespaces.md
CHANGED
@@ -68,7 +68,7 @@ end
|
|
68
68
|
|
69
69
|
**NOTE**: namespace support is an extension for `ActionPolicy::Behaviour` and could be included with `ActionPolicy::Behaviours::Namespaced` (included into Rails controllers and channel integrations by default).
|
70
70
|
|
71
|
-
## Namespace
|
71
|
+
## Namespace resolution cache
|
72
72
|
|
73
73
|
We cache namespaced policy resolution for better performance (it could affect performance when we look up a policy from a deeply nested module context, see the [benchmark](https://github.com/palkan/action_policy/blob/master/benchmarks/namespaced_lookup_cache.rb)).
|
74
74
|
|
data/docs/non_rails.md
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# Using with Ruby applications
|
2
2
|
|
3
3
|
Action Policy is designed to be independent of any framework and does not have specific dependencies on Ruby on Rails.
|
4
|
+
|
4
5
|
You can [write your policies](writing_policies.md) for non-Rails applications the same way as you would do for Rails applications.
|
5
6
|
|
6
|
-
In order to have `authorize!` / `allowed_to?` methods, you will have to include `ActionPolicy::Behaviour` into your class (where you want to perform authorization):
|
7
|
+
In order to have `authorize!` / `allowed_to?` / `authorized` methods, you will have to include [`ActionPolicy::Behaviour`](./behaviour.md) into your class (where you want to perform authorization):
|
7
8
|
|
8
9
|
```ruby
|
9
10
|
class PostUpdateAction
|
@@ -25,5 +26,3 @@ class PostUpdateAction
|
|
25
26
|
end
|
26
27
|
end
|
27
28
|
```
|
28
|
-
|
29
|
-
`ActionPolicy::Behaviour` provides `authorize` class-level method to configure [authorization context](authorization_context.rb) and two instance-level methods: `authorize!` and `allowed_to?`.
|
data/docs/pundit_migration.md
CHANGED
@@ -1,5 +1,80 @@
|
|
1
1
|
# Migrate from Pundit to Action Policy
|
2
2
|
|
3
|
-
|
3
|
+
Migration from Pundit to Action Policy could be done in a progressive way: first, we make Pundit polices and authorization helpers use Action Policy under the hood, then you can rewrite policies in the Action Policy way.
|
4
4
|
|
5
|
-
|
5
|
+
### Phase 1. Quacking like a Pundit.
|
6
|
+
|
7
|
+
#### Step 1. Prepare controllers.
|
8
|
+
|
9
|
+
- Remove `include Pundit` from ApplicationController
|
10
|
+
|
11
|
+
- Add `authorize` method:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
def authorize(record, rule = nil)
|
15
|
+
options = {}
|
16
|
+
options[:to] = rule unless rule.nil?
|
17
|
+
|
18
|
+
authorize! record, **options
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
- Configure [authorization context](authorization_context) if necessary, e.g. add `authorize :current_user, as: :user` to `ApplicationController` (**NOTE:** added automatically in Rails apps)
|
23
|
+
|
24
|
+
- Add `policy` and `policy_scope` helpers:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
helper_method :policy
|
28
|
+
helper_method :policy_scope
|
29
|
+
|
30
|
+
def policy(record)
|
31
|
+
policy_for(record)
|
32
|
+
end
|
33
|
+
|
34
|
+
def policy_scope(scope)
|
35
|
+
authorized scope
|
36
|
+
end
|
37
|
+
|
38
|
+
```
|
39
|
+
|
40
|
+
**NOTE**: `policy` defined above is not equal to `allowed_to?` since it doesn't take into account pre-checks.
|
41
|
+
|
42
|
+
#### Step 2. Prepare policies.
|
43
|
+
|
44
|
+
We assume that you have a base class for all your policies, e.g. `ApplicationPolicy`.
|
45
|
+
|
46
|
+
Then do the following:
|
47
|
+
- Add `include ActionPolicy::Policy::Core` to `ApplicationPolicy`
|
48
|
+
|
49
|
+
- Update `ApplicationPolicy#initialize`:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
def initialize(target, user:)
|
53
|
+
# ...
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
- [Rewrite scopes](scoping).
|
58
|
+
|
59
|
+
Unfortunately, there is no easy way to migrate Pundit class-based scope to Action Policies scopes.
|
60
|
+
|
61
|
+
#### Step 3. Replace RSpec helper:
|
62
|
+
|
63
|
+
We provide a Pundit-compatibile syntax for RSpec tests:
|
64
|
+
|
65
|
+
```
|
66
|
+
# Remove DSL
|
67
|
+
# require "pundit/rspec"
|
68
|
+
#
|
69
|
+
# Add Action Policy Pundit DSL
|
70
|
+
require "action_policy/rspec/pundit_syntax"
|
71
|
+
```
|
72
|
+
|
73
|
+
### Phase 2. No more Pundit.
|
74
|
+
|
75
|
+
When everything is green, it's time to fully migrate to ActionPolicy:
|
76
|
+
- make ApplicationPolicy inherit from `ActionPolicy::Base`
|
77
|
+
- migrate view helpers (from `policy(..)` to `allowed_to?`, from `policy_scope` to `authorized`)
|
78
|
+
- re-write specs using simple non-DSL syntax (or [Action Policy RSpec syntax](testing#rspec-dsl))
|
79
|
+
- add [authorization tests](testing#testing-authorization) (add `require 'action_policy/rspec'`)
|
80
|
+
- use [Reasons](), [I18n integration](i18n), [cache](caching) and other Action Policy features!
|
data/docs/quick_start.md
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
## Installation
|
4
4
|
|
5
|
-
|
5
|
+
Install Action Policy with RubyGems:
|
6
6
|
|
7
7
|
```ruby
|
8
8
|
gem install action_policy
|
9
9
|
```
|
10
10
|
|
11
|
-
Or add
|
11
|
+
Or add `action_policy` to your application's `Gemfile`:
|
12
12
|
|
13
13
|
```ruby
|
14
14
|
gem "action_policy"
|
@@ -22,10 +22,10 @@ And then execute:
|
|
22
22
|
|
23
23
|
The core component of Action Policy is a _policy class_. Policy class describes how you control access to resources.
|
24
24
|
|
25
|
-
We suggest
|
25
|
+
We suggest having a separate policy class for each resource and encourage you to follow these conventions:
|
26
26
|
- put policies into the `app/policies` folder (when using with Rails);
|
27
|
-
- name policies using the corresponding resource name (model name) with a `Policy` suffix, e.g. `Post -> PostPolicy`;
|
28
|
-
- name rules using a predicate form of the corresponding activity (typically, a controller's action), e.g. `PostsController#update ->
|
27
|
+
- name policies using the corresponding singular resource name (model name) with a `Policy` suffix, e.g. `Post -> PostPolicy`;
|
28
|
+
- name rules using a predicate form of the corresponding activity (typically, a controller's action), e.g. `PostsController#update -> PostPolicy#update?`.
|
29
29
|
|
30
30
|
We also recommend to use an application-specific `ApplicationPolicy` with a global configuration to inherit from:
|
31
31
|
|
data/docs/rails.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# Using with Rails
|
2
2
|
|
3
|
-
Action Policy seamlessly integrates Ruby on Rails applications
|
3
|
+
Action Policy seamlessly integrates with Ruby on Rails applications.
|
4
4
|
|
5
5
|
In most cases, you do not have to do anything except writing policy files and adding `authorize!` calls.
|
6
6
|
|
7
|
+
**NOTE:** both controllers and channels extensions are built on top of the Action Policy [behaviour](./behaviour.md) mixin.
|
8
|
+
|
7
9
|
## Controllers integration
|
8
10
|
|
9
11
|
Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
|
@@ -56,7 +58,8 @@ In that case, Action Policy tries to infer the resource class from the controlle
|
|
56
58
|
class PostsController < ApplicationPolicy
|
57
59
|
def index
|
58
60
|
# Uses Post class as a resource implicitly.
|
59
|
-
# NOTE: it just calls `controller_name.classify.safe_constantize
|
61
|
+
# NOTE: it just calls `controller_name.classify.safe_constantize`,
|
62
|
+
# you can override this by defining `implicit_authorization_target` method.
|
60
63
|
authorize!
|
61
64
|
end
|
62
65
|
end
|
data/docs/reasons.md
CHANGED
@@ -32,8 +32,6 @@ end
|
|
32
32
|
|
33
33
|
The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
|
34
34
|
|
35
|
-
**NOTE:** `full_messages` support hasn't been released yet. See [the issue](https://github.com/palkan/action_policy/issues/15).
|
36
|
-
|
37
35
|
You can also wrap _local_ rules into `allowed_to?` to populate reasons:
|
38
36
|
|
39
37
|
```ruby
|
@@ -55,9 +53,58 @@ p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
|
|
55
53
|
p ex.result.reasons.details #=> { stage: [:show?] }
|
56
54
|
```
|
57
55
|
|
56
|
+
## Detailed Reasons
|
57
|
+
|
58
|
+
**NOTE:** this feature hasn't been released yet and planned for 0.3.0 release.
|
59
|
+
You can use it now by installing the gem from GitHub master:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
gem "action_policy", github: "palkan/action_policy"
|
63
|
+
```
|
64
|
+
|
65
|
+
You can provide additional details to your failure reasons by using a `details: { ... }` option:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class ApplicantPolicy < ApplicationPolicy
|
69
|
+
def show?
|
70
|
+
allowed_to?(:show?, object.stage)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class StagePolicy < ApplicationPolicy
|
75
|
+
def show?
|
76
|
+
# Add stage title to the failure reason (if any)
|
77
|
+
# (could be used by client to show more descriptive message)
|
78
|
+
details[:title] = record.title
|
79
|
+
|
80
|
+
# then perform the checks
|
81
|
+
user.stages.where(id: record.id).exists?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# when accessing the reasons
|
86
|
+
p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }
|
87
|
+
```
|
88
|
+
|
89
|
+
**NOTE**: when using detailed reasons, the `details` array contains as the last element
|
90
|
+
a hash with ALL details reasons for the policy (in a form of `<rule> => <details>`).
|
58
91
|
|
92
|
+
The additional details are especially helpful when combined with localization, 'cause you can you them as interpolation data source for your translations. For example, for the above policy:
|
93
|
+
|
94
|
+
```yml
|
95
|
+
en:
|
96
|
+
policy:
|
97
|
+
stage:
|
98
|
+
show?: "The %{title} stage is not accessible"
|
99
|
+
```
|
100
|
+
|
101
|
+
And then when you call `full_messages`:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
p ex.result.reasons.full_messages #=> The Onboarding stage is not accessible
|
105
|
+
```
|
59
106
|
|
60
|
-
**What is the point of failure reasons?**
|
107
|
+
**P.S. What is the point of failure reasons?**
|
61
108
|
|
62
109
|
Failure reasons helps you to write _actionable_ error messages, i.e. to provide a user with helpful feedback.
|
63
110
|
|
data/docs/scoping.md
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# Scoping
|
2
|
+
|
3
|
+
**NOTE:** this feature hasn't been released yet and planned for 0.3.0 release.
|
4
|
+
You can use it now by installing the gem from GitHub master:
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
gem "action_policy", github: "palkan/action_policy"
|
8
|
+
```
|
9
|
+
|
10
|
+
By _scoping_ we mean an ability to use policies to _scope data_ (or _filter/modify/transform/choose-your-verb_).
|
11
|
+
|
12
|
+
The most common situation is when you want to _scope_ ActiveRecord relations depending
|
13
|
+
on the current user permissions. Without policies it could look like this:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class PostsController < ApplicationController
|
17
|
+
def index
|
18
|
+
@posts =
|
19
|
+
if current_user.admin?
|
20
|
+
Post.all
|
21
|
+
else
|
22
|
+
Post.where(user: current_user)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
That's a very simplified example. In practice scoping rules might be more complex, and it's likely that we would use them in multiple places.
|
29
|
+
|
30
|
+
Action Policy allows you to define scoping rules within a policy class and use them with the help of `authorized_scope` method (`authorized` alias is also available):
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class PostsController < ApplicationController
|
34
|
+
def index
|
35
|
+
@posts = authorized_scope(Post.all)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class PostPolicy < ApplicationPolicy
|
40
|
+
relation_scope do |relation|
|
41
|
+
next relation if user.admin?
|
42
|
+
relation.where(user: user)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
## Define scopes
|
48
|
+
|
49
|
+
To define scope you should use either `scope_for` or `smth_scope` methods in your policy:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class PostPolicy < ApplicationPolicy
|
53
|
+
# define a scope of a `relation` type
|
54
|
+
scope_for :relation do |relation|
|
55
|
+
relation.where(user: user)
|
56
|
+
end
|
57
|
+
|
58
|
+
# define a scope of `my_data` type,
|
59
|
+
# which acts on hashes
|
60
|
+
scope_for :my_data do |data|
|
61
|
+
next data if user.admin?
|
62
|
+
data.delete_if { |k, _| SENSITIVE_KEYS.include?(k) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
Scopes have _types_: different types of scopes are meant to be applied to different data types.
|
68
|
+
|
69
|
+
You can specify multiple scopes (_named scopes_) for the same type providing a scope name:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class EventPolicy < ApplictionPolicy
|
73
|
+
scope_for :relation, :own do |relation|
|
74
|
+
relation.where(owner: user)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
When the second argument is not specified, the `:default` is implied as the scope name.
|
80
|
+
|
81
|
+
Also, there are cases where it might be easier to add options to existing scope than create a new one.
|
82
|
+
|
83
|
+
For example, if you use soft-deletion and your logic inside a scope depends on if deleted records are included, you can add `with_deleted` option:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class PostPolicy < ApplicationPolicy
|
87
|
+
scope_for :relation do |relation, with_deleted: false|
|
88
|
+
rel = some_logic(relation)
|
89
|
+
with_deleted ? rel.with_deleted : rel
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
You can add as many options as you want:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class PostPolicy < ApplicationPolicy
|
98
|
+
scope_for :relation do |relation, with_deleted: false, magic_number: 42, some_required_option:|
|
99
|
+
# Your code
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
## Apply scopes
|
104
|
+
|
105
|
+
Action Policy behaviour (`ActionPolicy::Behaviour`) provides an `authorized` method which allows you to use scoping:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class PostsController < ApplicationController
|
109
|
+
def index
|
110
|
+
# The first argument is the target,
|
111
|
+
# which is passed to the scope block
|
112
|
+
#
|
113
|
+
# The second argument is the scope type
|
114
|
+
@posts = authorized_scope(Post, type: :relation)
|
115
|
+
#
|
116
|
+
# For named scopes provide `as` option
|
117
|
+
@events = authorized_scope(Event, type: :relation, as: :own)
|
118
|
+
#
|
119
|
+
# If you want to specify scope options provide `scope_options` option
|
120
|
+
@events = authorized_scope(Event, type: :relation, scope_options: {with_deleted: true})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
You can also specify additional options for policy class inference (see [behaviour docs](behaviour)). For example, to explicitly specify the policy class use:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
@posts = authorized_scope(Post, with: CustomPostPolicy)
|
129
|
+
```
|
130
|
+
|
131
|
+
## Using scopes within policy
|
132
|
+
|
133
|
+
You can also use scopes within policy classes using the same `authorized_scope` method.
|
134
|
+
For example:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
relation_scope(:edit) do |scope|
|
138
|
+
teachers = authorized_scope(Teacher.all, as: :edit)
|
139
|
+
scope
|
140
|
+
.joins(:teachers)
|
141
|
+
.where(teacher_id: teachers)
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
## Using scopes explicitly
|
146
|
+
|
147
|
+
To use scopes without including Action Policy [behaviour](behaviour)
|
148
|
+
do the following:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# initialize policy
|
152
|
+
policy = ApplicantPolicy.new(user: user)
|
153
|
+
# apply scope
|
154
|
+
policy.apply_scope(User.all, type: :relation)
|
155
|
+
```
|
156
|
+
|
157
|
+
## Scope type inference
|
158
|
+
|
159
|
+
Action Policy could look up a scope type if it's not specified and if _scope matchers_ were configured.
|
160
|
+
|
161
|
+
Scope matcher is an object that implements `#===` (_case equality_) or a Proc. You can define it within a policy class:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class ApplicationPolicy < ActionPolicy::Base
|
165
|
+
scope_matcher :relation, ActiveRecord::Relation
|
166
|
+
|
167
|
+
# use Proc to handle AR models classes
|
168
|
+
scope_matcher :relation, ->(target) { target < ActiveRecord::Base }
|
169
|
+
|
170
|
+
scope_matcher :custom, MyCustomClass
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
Adding a scope matcher also adds a DSL to define scope rules (just a syntax sugar):
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class ApplicationPolicy < ActionPolicy::Base
|
178
|
+
scope_matcher :relation, ActiveRecord::Relation
|
179
|
+
|
180
|
+
# now you can define scope rules like this
|
181
|
+
relation_scope { |relation| relation }
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
When `authorized_scope` is called without the explicit scope type, Action Policy uses matchers (in the order they're defined) to infer the type.
|
186
|
+
|
187
|
+
## Rails integration
|
188
|
+
|
189
|
+
Action Policy provides a couple of _scope matchers_ out-of-the-box for Active Record relations and Action Controller paramters.
|
190
|
+
|
191
|
+
### Active Record scopes
|
192
|
+
|
193
|
+
Scope type `:relation` is automatically applied to the object of `ActiveRecord::Relation` type.
|
194
|
+
|
195
|
+
To define Active Record scopes you can use `relation_scope` macro (which is just an alias for `scope :relation`) in your policy:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
class PostPolicy < ApplicationPolicy
|
199
|
+
# Equals `scope_for :active_record_relation do ...`
|
200
|
+
relation_scope do |scope|
|
201
|
+
if super_user? || admin?
|
202
|
+
scope
|
203
|
+
else
|
204
|
+
scope.joins(:accesses).where(accesses: {user_id: user.id})
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# define named scope
|
209
|
+
relation_scope(:own) do |scope|
|
210
|
+
next scope.none if user.guest?
|
211
|
+
scope.where(user: user)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
**NOTE:** the `:active_record_relation` scoping is used if and only if an `ActiveRecord::Relation` is passed to `authorized`:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
def index
|
220
|
+
# BAD: Post is not a relation; raises an exception
|
221
|
+
@posts = authorized_scope(Post)
|
222
|
+
|
223
|
+
# GOOD:
|
224
|
+
@posts = authorized_scope(Post.all)
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
### Action Controller parameters
|
229
|
+
|
230
|
+
Use scopes of type `:params` if your strong parameters filterings depend on the current user:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class UserPolicy < ApplicationPolicy
|
234
|
+
# Equals to `scope_for :action_controller_params do ...`
|
235
|
+
params_filter do |params|
|
236
|
+
if user.admin?
|
237
|
+
params.permit(:name, :email, :role)
|
238
|
+
else
|
239
|
+
params.permit(:name)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
params_filter(:update) do |params|
|
244
|
+
params.permit(:name)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
class UsersController < ApplicationController
|
249
|
+
def create
|
250
|
+
# Call `authorized_scope` on `params` object
|
251
|
+
@user = User.create!(authorized_scope(params.require(:user)))
|
252
|
+
# Or you can use `authorized` alias which fits this case better
|
253
|
+
@user = User.create!(authorized(params.require(:user)))
|
254
|
+
head :ok
|
255
|
+
end
|
256
|
+
|
257
|
+
def update
|
258
|
+
@user.update!(authorized_scope(params.require(:user), as: :update))
|
259
|
+
head :ok
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|