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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +233 -171
- data/LICENSE.txt +1 -1
- data/README.md +7 -11
- data/lib/action_policy.rb +7 -1
- data/lib/action_policy/behaviour.rb +22 -16
- data/lib/action_policy/behaviours/policy_for.rb +10 -3
- data/lib/action_policy/behaviours/scoping.rb +2 -1
- data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
- data/lib/action_policy/ext/module_namespace.rb +1 -6
- data/lib/action_policy/ext/policy_cache_key.rb +15 -33
- data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
- data/lib/action_policy/i18n.rb +1 -1
- data/lib/action_policy/lookup_chain.rb +41 -21
- data/lib/action_policy/policy/aliases.rb +7 -12
- data/lib/action_policy/policy/authorization.rb +14 -17
- data/lib/action_policy/policy/cache.rb +34 -18
- data/lib/action_policy/policy/core.rb +25 -12
- data/lib/action_policy/policy/defaults.rb +3 -9
- data/lib/action_policy/policy/execution_result.rb +3 -9
- data/lib/action_policy/policy/pre_check.rb +19 -58
- data/lib/action_policy/policy/reasons.rb +30 -20
- data/lib/action_policy/policy/scoping.rb +5 -6
- data/lib/action_policy/rails/controller.rb +6 -1
- data/lib/action_policy/rails/ext/active_record.rb +7 -0
- data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
- data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
- data/lib/action_policy/rspec/dsl.rb +3 -3
- data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
- data/lib/action_policy/testing.rb +1 -1
- data/lib/action_policy/utils/pretty_print.rb +21 -24
- data/lib/action_policy/utils/suggest_message.rb +1 -3
- data/lib/action_policy/version.rb +1 -1
- data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
- data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
- data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
- data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
- data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
- metadata +30 -119
- data/.gitattributes +0 -2
- data/.github/FUNDING.yml +0 -1
- data/.github/ISSUE_TEMPLATE.md +0 -18
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -54
- data/.tidelift.yml +0 -6
- data/.travis.yml +0 -31
- data/Gemfile +0 -22
- data/Rakefile +0 -27
- data/action_policy.gemspec +0 -44
- data/benchmarks/namespaced_lookup_cache.rb +0 -71
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +0 -1
- data/docs/README.md +0 -77
- data/docs/_sidebar.md +0 -27
- data/docs/aliases.md +0 -122
- data/docs/assets/docsify-search.js +0 -364
- data/docs/assets/docsify.min.js +0 -3
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/banner.png +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +0 -70
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +0 -35
- data/docs/assets/prism-ruby.min.js +0 -1
- data/docs/assets/styles.css +0 -347
- data/docs/assets/vue.min.css +0 -1
- data/docs/authorization_context.md +0 -92
- data/docs/behaviour.md +0 -113
- data/docs/caching.md +0 -273
- data/docs/controller_action_aliases.md +0 -109
- data/docs/custom_lookup_chain.md +0 -48
- data/docs/custom_policy.md +0 -53
- data/docs/debugging.md +0 -55
- data/docs/decorators.md +0 -27
- data/docs/favicon.ico +0 -0
- data/docs/graphql.md +0 -302
- data/docs/i18n.md +0 -44
- data/docs/index.html +0 -43
- data/docs/instrumentation.md +0 -84
- data/docs/lookup_chain.md +0 -17
- data/docs/namespaces.md +0 -77
- data/docs/non_rails.md +0 -28
- data/docs/pre_checks.md +0 -57
- data/docs/pundit_migration.md +0 -80
- data/docs/quick_start.md +0 -118
- data/docs/rails.md +0 -120
- data/docs/reasons.md +0 -120
- data/docs/scoping.md +0 -255
- data/docs/testing.md +0 -333
- data/docs/writing_policies.md +0 -107
- data/gemfiles/jruby.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails6.gemfile +0 -8
- data/gemfiles/railsmaster.gemfile +0 -6
- data/lib/action_policy/ext/string_match.rb +0 -14
- data/lib/action_policy/ext/yield_self_then.rb +0 -25
data/docs/testing.md
DELETED
@@ -1,333 +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
|
-
There is no single rule on how to test scopes, 'cause it dependes on the _nature_ of the scope.
|
116
|
-
|
117
|
-
Here's an example of RSpec tests for Active Record scoping rules:
|
118
|
-
|
119
|
-
```ruby
|
120
|
-
describe PostPolicy do
|
121
|
-
describe "relation scope" do
|
122
|
-
let(:user) { build_stubbed :user }
|
123
|
-
let(:context) { {user: user} }
|
124
|
-
|
125
|
-
# Feel free to replace with `before_all` from `test-prof`:
|
126
|
-
# https://test-prof.evilmartians.io/#/before_all
|
127
|
-
before do
|
128
|
-
create(:post, name: "A")
|
129
|
-
create(:post, name: "B", draft: true)
|
130
|
-
end
|
131
|
-
|
132
|
-
let(:target) do
|
133
|
-
# We want to make sure that only the records created
|
134
|
-
# for this test are affected, and they have a deterministic order
|
135
|
-
Post.where(name: %w[A B]).order(name: :asc)
|
136
|
-
end
|
137
|
-
|
138
|
-
subject { policy.apply_scope(target, type: :active_record_relation).pluck(:name) }
|
139
|
-
|
140
|
-
context "as user" do
|
141
|
-
it { is_expected.to eq(%w[A]) }
|
142
|
-
end
|
143
|
-
|
144
|
-
context "as manager" do
|
145
|
-
before { user.update!(role: :manager) }
|
146
|
-
|
147
|
-
it { is_expected.to eq(%w[A B]) }
|
148
|
-
end
|
149
|
-
|
150
|
-
context "as banned user" do
|
151
|
-
before { user.update!(banned: true) }
|
152
|
-
|
153
|
-
it { is_expected.to be_empty }
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
```
|
158
|
-
|
159
|
-
## Testing authorization
|
160
|
-
|
161
|
-
To test the act of authorization you have to make sure that the `authorize!` method is called with the appropriate arguments.
|
162
|
-
|
163
|
-
Action Policy provides tools for such kind of testing for Minitest and RSpec.
|
164
|
-
|
165
|
-
### Minitest
|
166
|
-
|
167
|
-
Include `ActionPolicy::TestHelper` to your test class and you'll be able to use
|
168
|
-
`assert_authorized_to` assertion:
|
169
|
-
|
170
|
-
```ruby
|
171
|
-
# in your controller
|
172
|
-
class PostsController < ApplicationController
|
173
|
-
def update
|
174
|
-
@post = Post.find(params[:id])
|
175
|
-
authorize! @post
|
176
|
-
if @post.update(post_params)
|
177
|
-
redirect_to @post
|
178
|
-
else
|
179
|
-
render :edit
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
# in your test
|
185
|
-
require "action_policy/test_helper"
|
186
|
-
|
187
|
-
class PostsControllerTest < ActionDispatch::IntegrationTest
|
188
|
-
include ActionPolicy::TestHelper
|
189
|
-
|
190
|
-
test "update is authorized" do
|
191
|
-
sign_in users(:john)
|
192
|
-
|
193
|
-
post = posts(:example)
|
194
|
-
|
195
|
-
assert_authorized_to(:update?, post, with: PostPolicy) do
|
196
|
-
patch :update, id: post.id, name: "Bob"
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
```
|
201
|
-
|
202
|
-
You can omit the policy (then it would be inferred from the target):
|
203
|
-
|
204
|
-
```ruby
|
205
|
-
assert_authorized_to(:update?, post) do
|
206
|
-
patch :update, id: post.id, name: "Bob"
|
207
|
-
end
|
208
|
-
```
|
209
|
-
|
210
|
-
### RSpec
|
211
|
-
|
212
|
-
Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
|
213
|
-
|
214
|
-
```ruby
|
215
|
-
require "action_policy/rspec"
|
216
|
-
```
|
217
|
-
|
218
|
-
Now you can use `be_authorized_to` matcher:
|
219
|
-
|
220
|
-
```ruby
|
221
|
-
describe PostsController do
|
222
|
-
subject { patch :update, id: post.id, params: params }
|
223
|
-
|
224
|
-
it "is authorized" do
|
225
|
-
expect { subject }.to be_authorized_to(:update?, post)
|
226
|
-
.with(PostPolicy)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
```
|
230
|
-
|
231
|
-
If you omit `.with(PostPolicy)` then the inferred policy for the target (`post`) would be used.
|
232
|
-
|
233
|
-
RSpec composed matchers are available as target:
|
234
|
-
|
235
|
-
```ruby
|
236
|
-
expect { subject }.to be_authorized_to(:show?, an_instance_of(Post))
|
237
|
-
```
|
238
|
-
|
239
|
-
## Testing scoping
|
240
|
-
|
241
|
-
Action Policy provides a way to test that a correct scoping has been applied during the code execution.
|
242
|
-
|
243
|
-
For example, you can test that in your `#index` action the correct scoping is used:
|
244
|
-
|
245
|
-
```ruby
|
246
|
-
class UsersController < ApplicationController
|
247
|
-
def index
|
248
|
-
@user = authorized(User.all)
|
249
|
-
end
|
250
|
-
end
|
251
|
-
```
|
252
|
-
|
253
|
-
### Minitest
|
254
|
-
|
255
|
-
Include `ActionPolicy::TestHelper` to your test class and you'll be able to use
|
256
|
-
`assert_have_authorized_scope` assertion:
|
257
|
-
|
258
|
-
```ruby
|
259
|
-
# in your test
|
260
|
-
require "action_policy/test_helper"
|
261
|
-
|
262
|
-
class UsersControllerTest < ActionDispatch::IntegrationTest
|
263
|
-
include ActionPolicy::TestHelper
|
264
|
-
|
265
|
-
test "index has authorized scope" do
|
266
|
-
sign_in users(:john)
|
267
|
-
|
268
|
-
assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
|
269
|
-
get :index
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
```
|
274
|
-
|
275
|
-
You can also specify `as` and `scope_options` options.
|
276
|
-
|
277
|
-
**NOTE:** both `type` and `with` params are required.
|
278
|
-
|
279
|
-
It's not possible to test that a scoped has been applied to a particular _target_ but we provide
|
280
|
-
a way to perform additional assertions against the matching target (if the assertion didn't fail):
|
281
|
-
|
282
|
-
```ruby
|
283
|
-
test "index has authorized scope" do
|
284
|
-
sign_in users(:john)
|
285
|
-
|
286
|
-
assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
|
287
|
-
get :index
|
288
|
-
end.with_target do |target|
|
289
|
-
# target is a object passed to `authorized` call
|
290
|
-
assert_equal User.all, target
|
291
|
-
end
|
292
|
-
end
|
293
|
-
```
|
294
|
-
|
295
|
-
### RSpec
|
296
|
-
|
297
|
-
Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
|
298
|
-
|
299
|
-
```ruby
|
300
|
-
require "action_policy/rspec"
|
301
|
-
```
|
302
|
-
|
303
|
-
Now you can use `have_authorized_scope` matcher:
|
304
|
-
|
305
|
-
```ruby
|
306
|
-
describe UsersController do
|
307
|
-
subject { get :index }
|
308
|
-
|
309
|
-
it "has authorized scope" do
|
310
|
-
expect { subject }.to have_authorized_scope(:active_record_relation)
|
311
|
-
.with(PostPolicy)
|
312
|
-
end
|
313
|
-
end
|
314
|
-
```
|
315
|
-
|
316
|
-
You can also add `.as(:named_scope)` and `with_scope_options(options_hash)` options.
|
317
|
-
|
318
|
-
RSpec composed matchers are available as scope options:
|
319
|
-
|
320
|
-
```ruby
|
321
|
-
expect { subject }.to have_authorized_scope(:scope)
|
322
|
-
.with_scope_options(matching(with_deleted: a_falsey_value))
|
323
|
-
```
|
324
|
-
|
325
|
-
You can use the `with_target` modifier to run additional expectations against the matching target (if the matcher didn't fail):
|
326
|
-
|
327
|
-
```ruby
|
328
|
-
expect { subject }.to have_authorized_scope(:scope)
|
329
|
-
.with_scope_options(matching(with_deleted: a_falsey_value))
|
330
|
-
.with_target { |target|
|
331
|
-
expect(target).to eq(User.all)
|
332
|
-
}
|
333
|
-
```
|
data/docs/writing_policies.md
DELETED
@@ -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).
|
data/gemfiles/jruby.gemfile
DELETED