action_policy 0.4.0 → 0.5.0

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -171
  3. data/LICENSE.txt +1 -1
  4. data/README.md +7 -11
  5. data/lib/action_policy.rb +7 -1
  6. data/lib/action_policy/behaviour.rb +22 -16
  7. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  8. data/lib/action_policy/behaviours/scoping.rb +2 -1
  9. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  10. data/lib/action_policy/ext/module_namespace.rb +1 -6
  11. data/lib/action_policy/ext/policy_cache_key.rb +15 -33
  12. data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
  13. data/lib/action_policy/i18n.rb +1 -1
  14. data/lib/action_policy/lookup_chain.rb +41 -21
  15. data/lib/action_policy/policy/aliases.rb +7 -12
  16. data/lib/action_policy/policy/authorization.rb +14 -17
  17. data/lib/action_policy/policy/cache.rb +34 -18
  18. data/lib/action_policy/policy/core.rb +25 -12
  19. data/lib/action_policy/policy/defaults.rb +3 -9
  20. data/lib/action_policy/policy/execution_result.rb +3 -9
  21. data/lib/action_policy/policy/pre_check.rb +19 -58
  22. data/lib/action_policy/policy/reasons.rb +30 -20
  23. data/lib/action_policy/policy/scoping.rb +5 -6
  24. data/lib/action_policy/rails/controller.rb +6 -1
  25. data/lib/action_policy/rails/ext/active_record.rb +7 -0
  26. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  27. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  28. data/lib/action_policy/rspec/dsl.rb +3 -3
  29. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  30. data/lib/action_policy/testing.rb +1 -1
  31. data/lib/action_policy/utils/pretty_print.rb +21 -24
  32. data/lib/action_policy/utils/suggest_message.rb +1 -3
  33. data/lib/action_policy/version.rb +1 -1
  34. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
  35. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  36. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  37. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  38. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  39. metadata +30 -119
  40. data/.gitattributes +0 -2
  41. data/.github/FUNDING.yml +0 -1
  42. data/.github/ISSUE_TEMPLATE.md +0 -18
  43. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  44. data/.gitignore +0 -15
  45. data/.rubocop.yml +0 -54
  46. data/.tidelift.yml +0 -6
  47. data/.travis.yml +0 -31
  48. data/Gemfile +0 -22
  49. data/Rakefile +0 -27
  50. data/action_policy.gemspec +0 -44
  51. data/benchmarks/namespaced_lookup_cache.rb +0 -71
  52. data/bin/console +0 -14
  53. data/bin/setup +0 -8
  54. data/docs/.nojekyll +0 -0
  55. data/docs/CNAME +0 -1
  56. data/docs/README.md +0 -77
  57. data/docs/_sidebar.md +0 -27
  58. data/docs/aliases.md +0 -122
  59. data/docs/assets/docsify-search.js +0 -364
  60. data/docs/assets/docsify.min.js +0 -3
  61. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  62. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  63. data/docs/assets/images/banner.png +0 -0
  64. data/docs/assets/images/cache.png +0 -0
  65. data/docs/assets/images/cache.svg +0 -70
  66. data/docs/assets/images/layer.png +0 -0
  67. data/docs/assets/images/layer.svg +0 -35
  68. data/docs/assets/prism-ruby.min.js +0 -1
  69. data/docs/assets/styles.css +0 -347
  70. data/docs/assets/vue.min.css +0 -1
  71. data/docs/authorization_context.md +0 -92
  72. data/docs/behaviour.md +0 -113
  73. data/docs/caching.md +0 -273
  74. data/docs/controller_action_aliases.md +0 -109
  75. data/docs/custom_lookup_chain.md +0 -48
  76. data/docs/custom_policy.md +0 -53
  77. data/docs/debugging.md +0 -55
  78. data/docs/decorators.md +0 -27
  79. data/docs/favicon.ico +0 -0
  80. data/docs/graphql.md +0 -302
  81. data/docs/i18n.md +0 -44
  82. data/docs/index.html +0 -43
  83. data/docs/instrumentation.md +0 -84
  84. data/docs/lookup_chain.md +0 -17
  85. data/docs/namespaces.md +0 -77
  86. data/docs/non_rails.md +0 -28
  87. data/docs/pre_checks.md +0 -57
  88. data/docs/pundit_migration.md +0 -80
  89. data/docs/quick_start.md +0 -118
  90. data/docs/rails.md +0 -120
  91. data/docs/reasons.md +0 -120
  92. data/docs/scoping.md +0 -255
  93. data/docs/testing.md +0 -333
  94. data/docs/writing_policies.md +0 -107
  95. data/gemfiles/jruby.gemfile +0 -8
  96. data/gemfiles/rails42.gemfile +0 -8
  97. data/gemfiles/rails6.gemfile +0 -8
  98. data/gemfiles/railsmaster.gemfile +0 -6
  99. data/lib/action_policy/ext/string_match.rb +0 -14
  100. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1,120 +0,0 @@
1
- # Using with Rails
2
-
3
- Action Policy seamlessly integrates with Ruby on Rails applications.
4
-
5
- In most cases, you do not have to do anything except writing policy files and adding `authorize!` calls.
6
-
7
- **NOTE:** both controllers and channels extensions are built on top of the Action Policy [behaviour](./behaviour.md) mixin.
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
-
16
- ## Controllers integration
17
-
18
- Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
19
-
20
- You can turn off this behaviour by setting `config.action_policy.controller_authorize_current_user = false` in `application.rb`, or override it:
21
-
22
- ```ruby
23
- class ApplicationController < ActionController::Base
24
- authorize :user, through: :my_current_user
25
- end
26
- ```
27
-
28
- > Read more about [authorization context](authorization_context.md).
29
-
30
- In case you don't want to include Action Policy to controllers at all,
31
- you can turn disable the integration by setting `config.action_policy.auto_inject_into_controller = false` in `application.rb`.
32
-
33
- ### `verify_authorized` hooks
34
-
35
- Usually, you need all of your actions to be authorized. Action Policy provides a controller hook which ensures that an `authorize!` call has been made during the action:
36
-
37
- ```ruby
38
- class ApplicationController < ActionController::Base
39
- # adds an after_action callback to verify
40
- # that `authorize!` has been called.
41
- verify_authorized
42
-
43
- # you can also pass additional options,
44
- # like with a usual callback
45
- verify_authorized except: :index
46
- end
47
- ```
48
-
49
- You can skip this check when necessary:
50
-
51
- ```ruby
52
- class PostsController < ApplicationController
53
- skip_verify_authorized only: :show
54
- end
55
- ```
56
-
57
- When an unauthorized action is encountered, the `ActionPolicy::UnauthorizedAction` error is raised.
58
-
59
- ### Resource-less `authorize!`
60
-
61
- You can also call `authorize!` without a resource specified.
62
- In that case, Action Policy tries to infer the resource class from the controller name:
63
-
64
- ```ruby
65
- class PostsController < ApplicationPolicy
66
- def index
67
- # Uses Post class as a resource implicitly.
68
- # NOTE: it just calls `controller_name.classify.safe_constantize`,
69
- # you can override this by defining `implicit_authorization_target` method.
70
- authorize!
71
- end
72
- end
73
- ```
74
-
75
- ### Usage with `API` and `Metal` controllers
76
-
77
- Action Policy is only included into `ActionController::Base`. If you want to use it with other base Rails controllers, you have to include it manually:
78
-
79
- ```ruby
80
- class ApiController < ApplicationController::API
81
- include ActionPolicy::Controller
82
-
83
- # NOTE: you have to provide authorization context manually as well
84
- authorize :user, through: :current_user
85
- end
86
- ```
87
-
88
- ## Channels integration
89
-
90
- Action Policy also integrates with Action Cable to help you authorize your channels actions:
91
-
92
- ```ruby
93
- class ChatChannel < ApplicationCable::Channel
94
- def follow(data)
95
- chat = Chat.find(data["chat_id"])
96
-
97
- # Verify against ChatPolicy#show? rule
98
- authorize! chat, to: :show?
99
- stream_from chat
100
- end
101
- end
102
- ```
103
-
104
- Action Policy assumes that you have `current_user` as a connection identifier.
105
-
106
- You can turn off this behaviour by setting `config.action_policy.channel_authorize_current_user = false` in `application.rb`, or override it:
107
-
108
- ```ruby
109
- module ApplicationCable
110
- class Channel < ActionCable::Channel::Base
111
- # assuming that identifier is called `user`
112
- authorize :user
113
- end
114
- end
115
- ```
116
-
117
- > Read more about [authorization context](authorization_context.md).
118
-
119
- In case you do not want to include Action Policy to channels at all,
120
- you can disable the integration by setting `config.action_policy.auto_inject_into_channel = false` in `application.rb`.
@@ -1,120 +0,0 @@
1
- # Failure Reasons
2
-
3
- When you have complex policy rules, it could be helpful to have an ability to define an exact reason for why a specific authorization was rejected.
4
-
5
- It is especially helpful when you compose policies (i.e., use one policy within another) or want
6
- to expose permissions to client applications (see [GraphQL](./graphql)).
7
-
8
- Action Policy allows you to track failed `allowed_to?` checks in your rules.
9
-
10
- Consider an example:
11
-
12
- ```ruby
13
- class ApplicantPolicy < ApplicationPolicy
14
- def show?
15
- user.has_permission?(:view_applicants) &&
16
- allowed_to?(:show?, object.stage)
17
- end
18
- end
19
- ```
20
-
21
- When `ApplicantPolicy#show?` check fails, the exception has the `result` object, which in its turn contains additional information about the failure (`reasons`):
22
-
23
- ```ruby
24
- class ApplicationController < ActionController::Base
25
- rescue_from ActionPolicy::Unauthorized do |ex|
26
- p ex.result.reasons.details #=> { stage: [:show?] }
27
-
28
- # or with i18n support
29
- p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
30
- end
31
- end
32
- ```
33
-
34
- The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
35
-
36
- You can also wrap _local_ rules into `allowed_to?` to populate reasons:
37
-
38
- ```ruby
39
- class ApplicantPolicy < ApplicationPolicy
40
- def show?
41
- allowed_to?(:view_applicants?) &&
42
- allowed_to?(:show?, object.stage)
43
- end
44
-
45
- def view_applicants?
46
- user.has_permission?(:view_applicants)
47
- end
48
- end
49
-
50
- # then the reasons object could be
51
- p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
52
-
53
- # or
54
- p ex.result.reasons.details #=> { stage: [:show?] }
55
- ```
56
-
57
- ## Detailed Reasons
58
-
59
- You can provide additional details to your failure reasons by using a `details: { ... }` option:
60
-
61
- ```ruby
62
- class ApplicantPolicy < ApplicationPolicy
63
- def show?
64
- allowed_to?(:show?, object.stage)
65
- end
66
- end
67
-
68
- class StagePolicy < ApplicationPolicy
69
- def show?
70
- # Add stage title to the failure reason (if any)
71
- # (could be used by client to show more descriptive message)
72
- details[:title] = record.title
73
-
74
- # then perform the checks
75
- user.stages.where(id: record.id).exists?
76
- end
77
- end
78
-
79
- # when accessing the reasons
80
- p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }
81
- ```
82
-
83
- **NOTE**: when using detailed reasons, the `details` array contains as the last element
84
- a hash with ALL details reasons for the policy (in a form of `<rule> => <details>`).
85
-
86
- 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:
87
-
88
- ```yml
89
- en:
90
- action_policy:
91
- policy:
92
- stage:
93
- show?: "The %{title} stage is not accessible"
94
- ```
95
-
96
- And then when you call `full_messages`:
97
-
98
- ```ruby
99
- p ex.result.reasons.full_messages #=> The Onboarding stage is not accessible
100
- ```
101
-
102
- **P.S. What is the point of failure reasons?**
103
-
104
- Failure reasons helps you to write _actionable_ error messages, i.e. to provide a user with helpful feedback.
105
-
106
- For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
107
-
108
- ```
109
- You don't have enough permissions to view applicants.
110
- Please, ask your manager to update your role.
111
- ```
112
-
113
- And when the reason is `StagePolicy#show?`:
114
-
115
- ```
116
- You don't have access to the stage XYZ.
117
- Please, ask your manager to grant access to this stage.
118
- ```
119
-
120
- Much more useful than just showing "You are not authorized to perform this action," isn't it?
@@ -1,255 +0,0 @@
1
- # Scoping
2
-
3
- By _scoping_ we mean an ability to use policies to _scope data_ (or _filter/modify/transform/choose-your-verb_).
4
-
5
- The most common situation is when you want to _scope_ ActiveRecord relations depending
6
- on the current user permissions. Without policies it could look like this:
7
-
8
- ```ruby
9
- class PostsController < ApplicationController
10
- def index
11
- @posts =
12
- if current_user.admin?
13
- Post.all
14
- else
15
- Post.where(user: current_user)
16
- end
17
- end
18
- end
19
- ```
20
-
21
- 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.
22
-
23
- 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):
24
-
25
- ```ruby
26
- class PostsController < ApplicationController
27
- def index
28
- @posts = authorized_scope(Post.all)
29
- end
30
- end
31
-
32
- class PostPolicy < ApplicationPolicy
33
- relation_scope do |relation|
34
- next relation if user.admin?
35
- relation.where(user: user)
36
- end
37
- end
38
- ```
39
-
40
- ## Define scopes
41
-
42
- To define scope you should use either `scope_for` or `smth_scope` methods in your policy:
43
-
44
- ```ruby
45
- class PostPolicy < ApplicationPolicy
46
- # define a scope of a `relation` type
47
- scope_for :relation do |relation|
48
- relation.where(user: user)
49
- end
50
-
51
- # define a scope of `my_data` type,
52
- # which acts on hashes
53
- scope_for :my_data do |data|
54
- next data if user.admin?
55
- data.delete_if { |k, _| SENSITIVE_KEYS.include?(k) }
56
- end
57
- end
58
- ```
59
-
60
- Scopes have _types_: different types of scopes are meant to be applied to different data types.
61
-
62
- You can specify multiple scopes (_named scopes_) for the same type providing a scope name:
63
-
64
- ```ruby
65
- class EventPolicy < ApplictionPolicy
66
- scope_for :relation, :own do |relation|
67
- relation.where(owner: user)
68
- end
69
- end
70
- ```
71
-
72
- When the second argument is not specified, the `:default` is implied as the scope name.
73
-
74
- Also, there are cases where it might be easier to add options to existing scope than create a new one.
75
-
76
- 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:
77
-
78
- ```ruby
79
- class PostPolicy < ApplicationPolicy
80
- scope_for :relation do |relation, with_deleted: false|
81
- rel = some_logic(relation)
82
- with_deleted ? rel.with_deleted : rel
83
- end
84
- end
85
- ```
86
-
87
- You can add as many options as you want:
88
-
89
- ```ruby
90
- class PostPolicy < ApplicationPolicy
91
- scope_for :relation do |relation, with_deleted: false, magic_number: 42, some_required_option:|
92
- # Your code
93
- end
94
- end
95
- ```
96
- ## Apply scopes
97
-
98
- Action Policy behaviour (`ActionPolicy::Behaviour`) provides an `authorized` method which allows you to use scoping:
99
-
100
- ```ruby
101
- class PostsController < ApplicationController
102
- def index
103
- # The first argument is the target,
104
- # which is passed to the scope block
105
- #
106
- # The second argument is the scope type
107
- @posts = authorized_scope(Post, type: :relation)
108
- #
109
- # For named scopes provide `as` option
110
- @events = authorized_scope(Event, type: :relation, as: :own)
111
- #
112
- # If you want to specify scope options provide `scope_options` option
113
- @events = authorized_scope(Event, type: :relation, scope_options: {with_deleted: true})
114
- end
115
- end
116
- ```
117
-
118
- You can also specify additional options for policy class inference (see [behaviour docs](behaviour)). For example, to explicitly specify the policy class use:
119
-
120
- ```ruby
121
- @posts = authorized_scope(Post, with: CustomPostPolicy)
122
- ```
123
-
124
- ## Using scopes within policy
125
-
126
- You can also use scopes within policy classes using the same `authorized_scope` method.
127
- For example:
128
-
129
- ```ruby
130
- relation_scope(:edit) do |scope|
131
- teachers = authorized_scope(Teacher.all, as: :edit)
132
- scope
133
- .joins(:teachers)
134
- .where(teacher_id: teachers)
135
- end
136
- ```
137
-
138
- ## Using scopes explicitly
139
-
140
- To use scopes without including Action Policy [behaviour](behaviour)
141
- do the following:
142
-
143
- ```ruby
144
- # initialize policy
145
- policy = ApplicantPolicy.new(user: user)
146
- # apply scope
147
- policy.apply_scope(User.all, type: :relation)
148
- ```
149
-
150
- ## Scope type inference
151
-
152
- Action Policy could look up a scope type if it's not specified and if _scope matchers_ were configured.
153
-
154
- Scope matcher is an object that implements `#===` (_case equality_) or a Proc. You can define it within a policy class:
155
-
156
- ```ruby
157
- class ApplicationPolicy < ActionPolicy::Base
158
- scope_matcher :relation, ActiveRecord::Relation
159
-
160
- # use Proc to handle AR models classes
161
- scope_matcher :relation, ->(target) { target < ActiveRecord::Base }
162
-
163
- scope_matcher :custom, MyCustomClass
164
- end
165
- ```
166
-
167
- Adding a scope matcher also adds a DSL to define scope rules (just a syntax sugar):
168
-
169
- ```ruby
170
- class ApplicationPolicy < ActionPolicy::Base
171
- scope_matcher :relation, ActiveRecord::Relation
172
-
173
- # now you can define scope rules like this
174
- relation_scope { |relation| relation }
175
- end
176
- ```
177
-
178
- When `authorized_scope` is called without the explicit scope type, Action Policy uses matchers (in the order they're defined) to infer the type.
179
-
180
- ## Rails integration
181
-
182
- Action Policy provides a couple of _scope matchers_ out-of-the-box for Active Record relations and Action Controller paramters.
183
-
184
- ### Active Record scopes
185
-
186
- Scope type `:relation` is automatically applied to the object of `ActiveRecord::Relation` type.
187
-
188
- To define Active Record scopes you can use `relation_scope` macro (which is just an alias for `scope :relation`) in your policy:
189
-
190
- ```ruby
191
- class PostPolicy < ApplicationPolicy
192
- # Equals `scope_for :active_record_relation do ...`
193
- relation_scope do |scope|
194
- if super_user? || admin?
195
- scope
196
- else
197
- scope.joins(:accesses).where(accesses: {user_id: user.id})
198
- end
199
- end
200
-
201
- # define named scope
202
- relation_scope(:own) do |scope|
203
- next scope.none if user.guest?
204
- scope.where(user: user)
205
- end
206
- end
207
- ```
208
-
209
- **NOTE:** the `:active_record_relation` scoping is used if and only if an `ActiveRecord::Relation` is passed to `authorized`:
210
-
211
- ```ruby
212
- def index
213
- # BAD: Post is not a relation; raises an exception
214
- @posts = authorized_scope(Post)
215
-
216
- # GOOD:
217
- @posts = authorized_scope(Post.all)
218
- end
219
- ```
220
-
221
- ### Action Controller parameters
222
-
223
- Use scopes of type `:params` if your strong parameters filterings depend on the current user:
224
-
225
- ```ruby
226
- class UserPolicy < ApplicationPolicy
227
- # Equals to `scope_for :action_controller_params do ...`
228
- params_filter do |params|
229
- if user.admin?
230
- params.permit(:name, :email, :role)
231
- else
232
- params.permit(:name)
233
- end
234
- end
235
-
236
- params_filter(:update) do |params|
237
- params.permit(:name)
238
- end
239
- end
240
-
241
- class UsersController < ApplicationController
242
- def create
243
- # Call `authorized_scope` on `params` object
244
- @user = User.create!(authorized_scope(params.require(:user)))
245
- # Or you can use `authorized` alias which fits this case better
246
- @user = User.create!(authorized(params.require(:user)))
247
- head :ok
248
- end
249
-
250
- def update
251
- @user.update!(authorized_scope(params.require(:user), as: :update))
252
- head :ok
253
- end
254
- end
255
- ```