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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -64
  3. data/.travis.yml +13 -10
  4. data/CHANGELOG.md +216 -1
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +1 -1
  7. data/Rakefile +10 -0
  8. data/action_policy.gemspec +5 -3
  9. data/benchmarks/namespaced_lookup_cache.rb +18 -22
  10. data/docs/README.md +3 -3
  11. data/docs/_sidebar.md +4 -0
  12. data/docs/aliases.md +9 -5
  13. data/docs/authorization_context.md +59 -1
  14. data/docs/behaviour.md +113 -0
  15. data/docs/caching.md +6 -4
  16. data/docs/custom_policy.md +1 -2
  17. data/docs/debugging.md +55 -0
  18. data/docs/decorators.md +27 -0
  19. data/docs/i18n.md +41 -2
  20. data/docs/instrumentation.md +70 -2
  21. data/docs/lookup_chain.md +5 -4
  22. data/docs/namespaces.md +1 -1
  23. data/docs/non_rails.md +2 -3
  24. data/docs/pundit_migration.md +77 -2
  25. data/docs/quick_start.md +5 -5
  26. data/docs/rails.md +5 -2
  27. data/docs/reasons.md +50 -3
  28. data/docs/scoping.md +262 -0
  29. data/docs/testing.md +232 -21
  30. data/docs/writing_policies.md +1 -1
  31. data/gemfiles/jruby.gemfile +3 -0
  32. data/gemfiles/rails42.gemfile +3 -0
  33. data/gemfiles/rails6.gemfile +8 -0
  34. data/gemfiles/railsmaster.gemfile +1 -1
  35. data/lib/action_policy.rb +3 -3
  36. data/lib/action_policy/authorizer.rb +12 -4
  37. data/lib/action_policy/base.rb +2 -0
  38. data/lib/action_policy/behaviour.rb +14 -3
  39. data/lib/action_policy/behaviours/memoized.rb +1 -1
  40. data/lib/action_policy/behaviours/policy_for.rb +12 -3
  41. data/lib/action_policy/behaviours/scoping.rb +32 -0
  42. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  43. data/lib/action_policy/ext/hash_transform_keys.rb +19 -0
  44. data/lib/action_policy/ext/module_namespace.rb +1 -1
  45. data/lib/action_policy/ext/policy_cache_key.rb +2 -1
  46. data/lib/action_policy/ext/proc_case_eq.rb +14 -0
  47. data/lib/action_policy/ext/string_constantize.rb +1 -0
  48. data/lib/action_policy/ext/symbol_classify.rb +22 -0
  49. data/lib/action_policy/i18n.rb +56 -0
  50. data/lib/action_policy/lookup_chain.rb +21 -3
  51. data/lib/action_policy/policy/cache.rb +10 -6
  52. data/lib/action_policy/policy/core.rb +31 -19
  53. data/lib/action_policy/policy/execution_result.rb +12 -0
  54. data/lib/action_policy/policy/pre_check.rb +2 -6
  55. data/lib/action_policy/policy/reasons.rb +99 -12
  56. data/lib/action_policy/policy/scoping.rb +165 -0
  57. data/lib/action_policy/rails/authorizer.rb +20 -0
  58. data/lib/action_policy/rails/controller.rb +4 -14
  59. data/lib/action_policy/rails/ext/active_record.rb +10 -0
  60. data/lib/action_policy/rails/policy/instrumentation.rb +24 -0
  61. data/lib/action_policy/rails/scope_matchers/action_controller_params.rb +19 -0
  62. data/lib/action_policy/rails/scope_matchers/active_record.rb +29 -0
  63. data/lib/action_policy/railtie.rb +29 -7
  64. data/lib/action_policy/rspec.rb +1 -0
  65. data/lib/action_policy/rspec/be_authorized_to.rb +1 -1
  66. data/lib/action_policy/rspec/dsl.rb +103 -0
  67. data/lib/action_policy/rspec/have_authorized_scope.rb +126 -0
  68. data/lib/action_policy/rspec/pundit_syntax.rb +1 -1
  69. data/lib/action_policy/test_helper.rb +69 -4
  70. data/lib/action_policy/testing.rb +54 -0
  71. data/lib/action_policy/utils/pretty_print.rb +137 -0
  72. data/lib/action_policy/utils/suggest_message.rb +21 -0
  73. data/lib/action_policy/version.rb +1 -1
  74. metadata +58 -11
@@ -1,5 +1,44 @@
1
1
  # I18n Support
2
2
 
3
- 🛠 **WORK IN PROGRESS**
3
+ `ActionPolicy` integrates with [`i18n`][] to support localizable `full_messages` for [reasons](./reasons.md) and the execution result's `message`:
4
4
 
5
- Follow [the issue](https://github.com/palkan/action_policy/issues/15).
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
@@ -1,5 +1,73 @@
1
1
  # Instrumentation
2
2
 
3
- 🛠 **WORK IN PROGRESS**
3
+ Action Policy integrates with [Rails instrumentation system](https://guides.rubyonrails.org/active_support_instrumentation.html), `ActiveSupport::Notifications`.
4
4
 
5
- See [the PR](https://github.com/palkan/action_policy/pull/4).
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
+ ```
@@ -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 responds to `policy_class`, then use it;
6
- 2. If the target's class responds to `policy_class`, then use it;
7
- 3. If the target's class responds to `policy_name`, then use it (the `policy_name` should end with `Policy` as it's not appended automatically);
8
- 4. Otherwise, use `#{target.class.name}Policy`.
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
 
@@ -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 resultion cache
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
 
@@ -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?`.
@@ -1,5 +1,80 @@
1
1
  # Migrate from Pundit to Action Policy
2
2
 
3
- 🛠 **WORK IN PROGRESS**
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
- Draft is available [here](https://gist.github.com/palkan/a4c482eeb8453ef6b103ee05f8c2f077).
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!
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## Installation
4
4
 
5
- To install Action Policy with RubyGems:
5
+ Install Action Policy with RubyGems:
6
6
 
7
7
  ```ruby
8
8
  gem install action_policy
9
9
  ```
10
10
 
11
- Or add this line to your application's `Gemfile`:
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 that you have a separate policy class for each resource and encourage you to follow the conventions:
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 -> PostsPolicy#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
 
@@ -1,9 +1,11 @@
1
1
  # Using with Rails
2
2
 
3
- Action Policy seamlessly integrates Ruby on Rails applications seamlessly.
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
@@ -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
 
@@ -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
+ ```