action_policy 0.4.4 → 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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +203 -174
  3. data/README.md +5 -4
  4. data/lib/action_policy.rb +7 -1
  5. data/lib/action_policy/behaviour.rb +22 -16
  6. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  7. data/lib/action_policy/behaviours/scoping.rb +2 -1
  8. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  9. data/lib/action_policy/ext/module_namespace.rb +1 -6
  10. data/lib/action_policy/ext/policy_cache_key.rb +10 -30
  11. data/lib/action_policy/i18n.rb +1 -1
  12. data/lib/action_policy/lookup_chain.rb +29 -15
  13. data/lib/action_policy/policy/aliases.rb +7 -12
  14. data/lib/action_policy/policy/authorization.rb +8 -7
  15. data/lib/action_policy/policy/cache.rb +11 -17
  16. data/lib/action_policy/policy/core.rb +25 -12
  17. data/lib/action_policy/policy/defaults.rb +3 -9
  18. data/lib/action_policy/policy/execution_result.rb +3 -9
  19. data/lib/action_policy/policy/pre_check.rb +19 -58
  20. data/lib/action_policy/policy/reasons.rb +29 -19
  21. data/lib/action_policy/policy/scoping.rb +5 -6
  22. data/lib/action_policy/rails/controller.rb +6 -1
  23. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  24. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  25. data/lib/action_policy/rspec/dsl.rb +1 -1
  26. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  27. data/lib/action_policy/utils/pretty_print.rb +21 -24
  28. data/lib/action_policy/utils/suggest_message.rb +1 -3
  29. data/lib/action_policy/version.rb +1 -1
  30. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  31. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  32. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  33. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  34. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  35. metadata +29 -119
  36. data/.gitattributes +0 -2
  37. data/.github/ISSUE_TEMPLATE.md +0 -21
  38. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  39. data/.github/bug_report_template.rb +0 -175
  40. data/.gitignore +0 -15
  41. data/.rubocop.yml +0 -54
  42. data/.tidelift.yml +0 -6
  43. data/.travis.yml +0 -31
  44. data/Gemfile +0 -22
  45. data/Rakefile +0 -27
  46. data/action_policy.gemspec +0 -44
  47. data/benchmarks/namespaced_lookup_cache.rb +0 -74
  48. data/benchmarks/pre_checks.rb +0 -73
  49. data/bin/console +0 -14
  50. data/bin/setup +0 -8
  51. data/docs/.nojekyll +0 -0
  52. data/docs/CNAME +0 -1
  53. data/docs/README.md +0 -79
  54. data/docs/_sidebar.md +0 -27
  55. data/docs/aliases.md +0 -122
  56. data/docs/assets/docsify-search.js +0 -364
  57. data/docs/assets/docsify.min.js +0 -3
  58. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  59. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  60. data/docs/assets/images/banner.png +0 -0
  61. data/docs/assets/images/cache.png +0 -0
  62. data/docs/assets/images/cache.svg +0 -70
  63. data/docs/assets/images/layer.png +0 -0
  64. data/docs/assets/images/layer.svg +0 -35
  65. data/docs/assets/prism-ruby.min.js +0 -1
  66. data/docs/assets/styles.css +0 -347
  67. data/docs/assets/vue.min.css +0 -1
  68. data/docs/authorization_context.md +0 -92
  69. data/docs/behaviour.md +0 -113
  70. data/docs/caching.md +0 -291
  71. data/docs/controller_action_aliases.md +0 -109
  72. data/docs/custom_lookup_chain.md +0 -48
  73. data/docs/custom_policy.md +0 -53
  74. data/docs/debugging.md +0 -55
  75. data/docs/decorators.md +0 -27
  76. data/docs/favicon.ico +0 -0
  77. data/docs/graphql.md +0 -302
  78. data/docs/i18n.md +0 -44
  79. data/docs/index.html +0 -43
  80. data/docs/instrumentation.md +0 -84
  81. data/docs/lookup_chain.md +0 -22
  82. data/docs/namespaces.md +0 -77
  83. data/docs/non_rails.md +0 -28
  84. data/docs/pre_checks.md +0 -57
  85. data/docs/pundit_migration.md +0 -80
  86. data/docs/quick_start.md +0 -118
  87. data/docs/rails.md +0 -120
  88. data/docs/reasons.md +0 -120
  89. data/docs/scoping.md +0 -255
  90. data/docs/testing.md +0 -390
  91. data/docs/writing_policies.md +0 -107
  92. data/gemfiles/jruby.gemfile +0 -8
  93. data/gemfiles/rails42.gemfile +0 -9
  94. data/gemfiles/rails6.gemfile +0 -8
  95. data/gemfiles/railsmaster.gemfile +0 -6
  96. data/lib/action_policy/ext/string_match.rb +0 -14
  97. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1,390 +0,0 @@
1
- # Testing
2
-
3
- Authorization is one of the crucial parts of your application. Hence, it should be thoroughly tested (that is the place where 100% coverage makes sense).
4
-
5
- When you use policies for authorization, it is possible to split testing into two parts:
6
- - Test the policy class itself
7
- - Test that **the required authorization is performed** within your authorization layer (controller, channel, etc.)
8
- - Test that **the required scoping has been applied**.
9
-
10
- ## Testing policies
11
-
12
- You can test policies as plain-old Ruby classes, no special tooling is required.
13
-
14
- Consider an RSpec example:
15
-
16
- ```ruby
17
- describe PostPolicy do
18
- let(:user) { build_stubbed(:user) }
19
- let(:post) { build_stubbed(:post) }
20
-
21
- let(:policy) { described_class.new(post, user: user) }
22
-
23
- describe "#update?" do
24
- subject { policy.apply(:update?) }
25
-
26
- it "returns false when the user is not admin nor author" do
27
- is_expected.to eq false
28
- end
29
-
30
- context "when the user is admin" do
31
- let(:user) { build_stubbed(:user, :admin) }
32
-
33
- it { is_expected.to eq true }
34
- end
35
-
36
- context "when the user is an author" do
37
- let(:post) { build_stubbed(:post, user: user) }
38
-
39
- it { is_expected.to eq true }
40
- end
41
- end
42
- end
43
- ```
44
-
45
- ### RSpec DSL
46
-
47
- We also provide a simple RSpec DSL which aims to reduce the boilerplate when writing
48
- policies specs.
49
-
50
- Example:
51
-
52
- ```ruby
53
- # Add this to your spec_helper.rb / rails_helper.rb
54
- require "action_policy/rspec/dsl"
55
-
56
- describe PostPolicy do
57
- let(:user) { build_stubbed :user }
58
- # `record` must be defined – it is the authorization target
59
- let(:record) { build_stubbed :post, draft: false }
60
-
61
- # `context` is the authorization context
62
- let(:context) { {user: user} }
63
-
64
- # `describe_rule` is a combination of
65
- # `describe` and `subject { ... }` (returns the result of
66
- # applying the rule to the record)
67
- describe_rule :show? do
68
- # `succeed` is `context` + `specify`, which checks
69
- # that the result of application is successful
70
- succeed "when post is published"
71
-
72
- # `succeed` is `context` + `specify`, which checks
73
- # that the result of application wasn't successful
74
- failed "when post is draft" do
75
- before { post.draft = false }
76
-
77
- succeed "when user is a manager" do
78
- before { user.role = "manager" }
79
- end
80
- end
81
- end
82
- end
83
- ```
84
-
85
- If test failed the exception message includes the result and [failure reasons](reasons) (if any):
86
-
87
- ```
88
- 1) PostPolicy#show? when post is draft
89
- Failure/Error: ...
90
-
91
- Expected to fail but succeed:
92
- <PostPolicy#show?: true (reasons: ...)>
93
- ```
94
-
95
- If you have [debugging utils](debugging) installed the message also includes the _annotated_
96
- source code of the policy rule:
97
-
98
- ```
99
- 1) UserPolicy#manage? when post is draft
100
- Failure/Error: ...
101
-
102
- Expected to fail but succeed:
103
- <PostPolicy#show?: true (reasons: ...)>
104
- ↳ user.admin? #=> true
105
- OR
106
- !record.draft? #=> false
107
- ```
108
-
109
- **NOTE:** DSL for focusing or skipping examples and groups is also available (e.g. `xdescribe_rule`, `fsucceed`, etc.).
110
-
111
- **NOTE:** the DSL is included only to example with the tag `type: :policy` or in the `spec/policies` folder. If you want to add this DSL to other examples, add `include ActionPolicy::RSpec::PolicyExampleGroup`.
112
-
113
- ### Testing scopes
114
-
115
- #### Active Record relation example
116
-
117
- There is no single rule on how to test scopes, 'cause it dependes on the _nature_ of the scope.
118
-
119
- Here's an example of RSpec tests for Active Record scoping rules:
120
-
121
- ```ruby
122
- describe PostPolicy do
123
- describe "relation scope" do
124
- let(:user) { build_stubbed :user }
125
- let(:context) { {user: user} }
126
-
127
- # Feel free to replace with `before_all` from `test-prof`:
128
- # https://test-prof.evilmartians.io/#/before_all
129
- before do
130
- create(:post, name: "A")
131
- create(:post, name: "B", draft: true)
132
- end
133
-
134
- let(:target) do
135
- # We want to make sure that only the records created
136
- # for this test are affected, and they have a deterministic order
137
- Post.where(name: %w[A B]).order(name: :asc)
138
- end
139
-
140
- subject { policy.apply_scope(target, type: :active_record_relation).pluck(:name) }
141
-
142
- context "as user" do
143
- it { is_expected.to eq(%w[A]) }
144
- end
145
-
146
- context "as manager" do
147
- before { user.update!(role: :manager) }
148
-
149
- it { is_expected.to eq(%w[A B]) }
150
- end
151
-
152
- context "as banned user" do
153
- before { user.update!(banned: true) }
154
-
155
- it { is_expected.to be_empty }
156
- end
157
- end
158
- end
159
- ```
160
-
161
- #### Action Controller params example
162
-
163
- Here's an example of RSpec tests for Action Controller parameters scoping rules:
164
-
165
- ```ruby
166
- describe PostPolicy do
167
- describe "params scope" do
168
- let(:user) { build_stubbed :user }
169
- let(:context) { {user: user} }
170
-
171
- let(:params) { {name: "a", password: "b"} }
172
- let(:target) { ActionController::Parameters.new(params) }
173
-
174
- # it's easier to asses the hash representation, not the AC::Params object
175
- subject { policy.apply_scope(target, type: :action_controller_params).to_h }
176
-
177
- context "as user" do
178
- it { is_expected.to eq({name: "a"}) }
179
- end
180
-
181
- context "as manager" do
182
- before { user.update!(role: :manager) }
183
-
184
- it { is_expected.to eq({name: "a", password: "b"}) }
185
- end
186
- end
187
- end
188
- ```
189
-
190
- ## Testing authorization
191
-
192
- To test the act of authorization you have to make sure that the `authorize!` method is called with the appropriate arguments.
193
-
194
- Action Policy provides tools for such kind of testing for Minitest and RSpec.
195
-
196
- ### Minitest
197
-
198
- Include `ActionPolicy::TestHelper` to your test class and you'll be able to use
199
- `assert_authorized_to` assertion:
200
-
201
- ```ruby
202
- # in your controller
203
- class PostsController < ApplicationController
204
- def update
205
- @post = Post.find(params[:id])
206
- authorize! @post
207
- if @post.update(post_params)
208
- redirect_to @post
209
- else
210
- render :edit
211
- end
212
- end
213
- end
214
-
215
- # in your test
216
- require "action_policy/test_helper"
217
-
218
- class PostsControllerTest < ActionDispatch::IntegrationTest
219
- include ActionPolicy::TestHelper
220
-
221
- test "update is authorized" do
222
- sign_in users(:john)
223
-
224
- post = posts(:example)
225
-
226
- assert_authorized_to(:update?, post, with: PostPolicy) do
227
- patch :update, id: post.id, name: "Bob"
228
- end
229
- end
230
- end
231
- ```
232
-
233
- You can omit the policy (then it would be inferred from the target):
234
-
235
- ```ruby
236
- assert_authorized_to(:update?, post) do
237
- patch :update, id: post.id, name: "Bob"
238
- end
239
- ```
240
-
241
- ### RSpec
242
-
243
- Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
244
-
245
- ```ruby
246
- require "action_policy/rspec"
247
- ```
248
-
249
- Now you can use `be_authorized_to` matcher:
250
-
251
- ```ruby
252
- describe PostsController do
253
- subject { patch :update, id: post.id, params: params }
254
-
255
- it "is authorized" do
256
- expect { subject }.to be_authorized_to(:update?, post)
257
- .with(PostPolicy)
258
- end
259
- end
260
- ```
261
-
262
- If you omit `.with(PostPolicy)` then the inferred policy for the target (`post`) would be used.
263
-
264
- RSpec composed matchers are available as target:
265
-
266
- ```ruby
267
- expect { subject }.to be_authorized_to(:show?, an_instance_of(Post))
268
- ```
269
-
270
- ## Testing scoping
271
-
272
- Action Policy provides a way to test that a correct scoping has been applied during the code execution.
273
-
274
- For example, you can test that in your `#index` action the correct scoping is used:
275
-
276
- ```ruby
277
- class UsersController < ApplicationController
278
- def index
279
- @user = authorized(User.all)
280
- end
281
- end
282
- ```
283
-
284
- ### Minitest
285
-
286
- Include `ActionPolicy::TestHelper` to your test class and you'll be able to use
287
- `assert_have_authorized_scope` assertion:
288
-
289
- ```ruby
290
- # in your test
291
- require "action_policy/test_helper"
292
-
293
- class UsersControllerTest < ActionDispatch::IntegrationTest
294
- include ActionPolicy::TestHelper
295
-
296
- test "index has authorized scope" do
297
- sign_in users(:john)
298
-
299
- assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
300
- get :index
301
- end
302
- end
303
- end
304
- ```
305
-
306
- You can also specify `as` and `scope_options` options.
307
-
308
- **NOTE:** both `type` and `with` params are required.
309
-
310
- It's not possible to test that a scoped has been applied to a particular _target_ but we provide
311
- a way to perform additional assertions against the matching target (if the assertion didn't fail):
312
-
313
- ```ruby
314
- test "index has authorized scope" do
315
- sign_in users(:john)
316
-
317
- assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
318
- get :index
319
- end.with_target do |target|
320
- # target is a object passed to `authorized` call
321
- assert_equal User.all, target
322
- end
323
- end
324
- ```
325
-
326
- ### RSpec
327
-
328
- Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
329
-
330
- ```ruby
331
- require "action_policy/rspec"
332
- ```
333
-
334
- Now you can use `have_authorized_scope` matcher:
335
-
336
- ```ruby
337
- describe UsersController do
338
- subject { get :index }
339
-
340
- it "has authorized scope" do
341
- expect { subject }.to have_authorized_scope(:active_record_relation)
342
- .with(PostPolicy)
343
- end
344
- end
345
- ```
346
-
347
- You can also add `.as(:named_scope)` and `with_scope_options(options_hash)` options.
348
-
349
- RSpec composed matchers are available as scope options:
350
-
351
- ```ruby
352
- expect { subject }.to have_authorized_scope(:scope)
353
- .with_scope_options(matching(with_deleted: a_falsey_value))
354
- ```
355
-
356
- You can use the `with_target` modifier to run additional expectations against the matching target (if the matcher didn't fail):
357
-
358
- ```ruby
359
- expect { subject }.to have_authorized_scope(:scope)
360
- .with_scope_options(matching(with_deleted: a_falsey_value))
361
- .with_target { |target|
362
- expect(target).to eq(User.all)
363
- }
364
- ```
365
-
366
-
367
- ## Testing views
368
-
369
- When you test views that call policies methods as `allowed_to?`, your may have `Missing policy authorization context: user` error.
370
- You may need to stub `current_user` to resolve the issue.
371
-
372
- Consider an RSpec example:
373
-
374
- ```ruby
375
- describe "users/index.html.slim" do
376
- let(:user) { build_stubbed :user }
377
- let(:users) { create_list(:user, 2) }
378
-
379
- before do
380
- allow(controller).to receive(:current_user).and_return(user)
381
-
382
- assign :users, users
383
- render
384
- end
385
-
386
- describe "displays user#index correctly" do
387
- it { expect(rendered).to have_link(users.first.email, href: edit_user_path(users.first)) }
388
- end
389
- end
390
- ```
@@ -1,107 +0,0 @@
1
- # Writing Policies
2
-
3
- Policy class contains predicate methods (_rules_) which are used to authorize activities.
4
-
5
- A Policy is instantiated with the target `record` (authorization object) and the [authorization context](authorization_context.md) (by default equals to `user`):
6
-
7
- ```ruby
8
- class PostPolicy < ActionPolicy::Base
9
- def index?
10
- # allow everyone to perform "index" activity on posts
11
- true
12
- end
13
-
14
- def update?
15
- # here we can access our context and record
16
- user.admin? || (user.id == record.user_id)
17
- end
18
- end
19
- ```
20
-
21
- ## Initializing policies
22
-
23
- **NOTE**: it is not recommended to manually initialize policy objects and use them directly (one exclusion–[tests](testing.md)). Use [`authorize!` / `allowed_to?` methods](./behaviour.md#authorize) instead.
24
-
25
- To initialize policy object, you should specify target record and context:
26
-
27
- ```ruby
28
- policy = PostPolicy.new(post, user: user)
29
-
30
- # simply call rule method
31
- policy.update?
32
- ```
33
-
34
- You can omit the first argument (in that case `record` would be `nil`).
35
-
36
- Instead of calling rules directly, it is better to call the `apply` method (which wraps rule method with some useful functionality, such as [caching](caching.md), [pre-checks](pre_checks.md), and [failure reasons tracking](reasons.md)):
37
-
38
- ```ruby
39
- policy.apply(:update?)
40
- ```
41
-
42
- ## Calling other policies
43
-
44
- Sometimes it is useful to call other resources policies from within a policy. Action Policy provides the `allowed_to?` method as a part of `ActionPolicy::Base`:
45
-
46
- ```ruby
47
- class CommentPolicy < ApplicationPolicy
48
- def update?
49
- user.admin? || (user.id == record.id) ||
50
- allowed_to?(:update?, record.post)
51
- end
52
- end
53
- ```
54
-
55
- You can also specify all the usual options (such as `with`).
56
-
57
- There is also a `check?` method which is just an "alias"\* for `allowed_to?` added for better readability:
58
-
59
- ```ruby
60
- class PostPolicy < ApplicationPolicy
61
- def show?
62
- user.admin? || check?(:publicly_visible?)
63
- end
64
-
65
- def publicly_visible?
66
- # ...
67
- end
68
- end
69
- ```
70
-
71
- \* It's not a Ruby _alias_ but a wrapper; we can't use `alias` or `alias_method`, 'cause `allowed_to?` could be extended by some extensions.
72
-
73
- ## Identifiers
74
-
75
- Each policy class has an `identifier`, which is by default just an underscored class name:
76
-
77
- ```ruby
78
- class CommentPolicy < ApplicationPolicy
79
- end
80
-
81
- CommentPolicy.identifier #=> :comment
82
- ```
83
-
84
- For namespaced policies it has a form of:
85
-
86
- ```ruby
87
- module ActiveAdmin
88
- class UserPolicy < ApplicationPolicy
89
- end
90
- end
91
-
92
- ActiveAdmin::UserPolicy.identifier # => :"active_admin/user"
93
- ```
94
-
95
- You can specify your own identifier:
96
-
97
- ```ruby
98
- module MyVeryLong
99
- class LongLongNamePolicy < ApplicationPolicy
100
- self.identifier = :long_name
101
- end
102
- end
103
-
104
- MyVeryLong::LongLongNamePolicy.identifier #=> :long_name
105
- ```
106
-
107
- Identifiers are required for some modules, such as [failure reasons tracking](reasons.md) and [i18n](i18n.md).