action_policy 0.2.4 → 0.3.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
```
|