pundit 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/CHANGELOG.md +16 -1
- data/CODE_OF_CONDUCT.md +28 -0
- data/CONTRIBUTING.md +33 -0
- data/README.md +107 -38
- data/lib/generators/pundit/install/templates/application_policy.rb +0 -1
- data/lib/generators/rspec/templates/policy_spec.rb +4 -4
- data/lib/generators/test_unit/templates/policy_test.rb +2 -2
- data/lib/pundit.rb +82 -21
- data/lib/pundit/policy_finder.rb +10 -4
- data/lib/pundit/rspec.rb +8 -4
- data/lib/pundit/version.rb +1 -1
- data/pundit.gemspec +1 -0
- data/spec/policies/post_policy_spec.rb +1 -1
- data/spec/pundit_spec.rb +123 -13
- data/spec/spec_helper.rb +44 -12
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2e10a94ed9f8cbd38d1c51f38344eedd7d680d8
|
4
|
+
data.tar.gz: 89b88467b3f85513d764ff7f2357f98dfa078b5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78331e942eabf7eb0b0990e0760481498cd056f10f3b11ecc7f9fb9718ae6532a1bd64a3682d012d8245ab019cf39f2e8671dff82e55dee1bf4e76afcf62dd05
|
7
|
+
data.tar.gz: c95f7e2fb27998009aeb6948464ce550938fc59a21d9169d0def6df7b3e467140f2d00fa198b97adce8cb08d232bd42d95cda46ab84813e08f305cc2277aee40
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
# Pundit
|
2
2
|
|
3
|
-
## 0.
|
3
|
+
## 1.0.0 (2015-04-19)
|
4
|
+
|
5
|
+
- Caches policy scopes and policies.
|
6
|
+
- Explicitly setting the policy for the controller via `controller.policy = foo` has been removed. Instead use `controller.policies[record] = foo`.
|
7
|
+
- Explicitly setting the policy scope for the controller via `controller.policy_policy = foo` has been removed. Instead use `controller.policy_scopes[scope] = foo`.
|
8
|
+
- Add `permitted_attributes` helper to fetch attributes from policy.
|
9
|
+
- Add `pundit_policy_authorized?` and `pundit_policy_scoped?` methods.
|
10
|
+
- Instance variables are prefixed to avoid collisions.
|
11
|
+
- Add `Pundit.authorize` method.
|
12
|
+
- Add `skip_authorization` and `skip_policy_scope` helpers.
|
13
|
+
- Better errors when checking multiple permissions in RSpec tests.
|
14
|
+
- Better errors in case `nil` is passed to `policy` or `policy_scope`.
|
15
|
+
- Use `inpect` when printing object for better errors.
|
16
|
+
- Dropped official support for Ruby 1.9.3
|
17
|
+
|
18
|
+
## 0.3.0 (2014-08-22)
|
4
19
|
|
5
20
|
- Extend the default `ApplicationPolicy` with an `ApplicationPolicy::Scope` (#120)
|
6
21
|
- Fix RSpec 3 deprecation warnings for built-in matchers (#162)
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all
|
4
|
+
people who contribute through reporting issues, posting feature requests,
|
5
|
+
updating documentation, submitting pull requests or patches, and other
|
6
|
+
activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, age, or religion.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include the use of sexual
|
14
|
+
language or imagery, derogatory comments or personal attacks, trolling, public
|
15
|
+
or private harassment, insults, or other unprofessional conduct.
|
16
|
+
|
17
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
18
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
19
|
+
that are not aligned to this Code of Conduct. Project maintainers who do not
|
20
|
+
follow the Code of Conduct may be removed from the project team.
|
21
|
+
|
22
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
23
|
+
reported by opening an issue or contacting one or more of the project
|
24
|
+
maintainers.
|
25
|
+
|
26
|
+
This Code of Conduct is adapted from the [Contributor
|
27
|
+
Covenant](http:contributor-covenant.org), version 1.0.0, available at
|
28
|
+
[http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
## Security issues
|
2
|
+
|
3
|
+
If you have found a security related issue, please do not file an issue on
|
4
|
+
GitHub or send a PR addressing the issue. Contact
|
5
|
+
[Jonas](mailto:jonas.nicklas@gmail.com) directly. You will be given public
|
6
|
+
credit for your disclosure.
|
7
|
+
|
8
|
+
## Reporting issues
|
9
|
+
|
10
|
+
Please try to answer the following questions in your bug report:
|
11
|
+
|
12
|
+
- What did you do?
|
13
|
+
- What did you expect to happen?
|
14
|
+
- What happened instead?
|
15
|
+
|
16
|
+
Make sure to include as much relevant information as possible. Ruby version,
|
17
|
+
Pundit version, OS version and any stack traces you have are very valuable.
|
18
|
+
|
19
|
+
## Pull Requests
|
20
|
+
|
21
|
+
- **Add tests!** Your patch won't be accepted if it doesn't have tests.
|
22
|
+
|
23
|
+
- **Document any change in behaviour**. Make sure the README and any other
|
24
|
+
relevant documentation are kept up-to-date.
|
25
|
+
|
26
|
+
- **Create topic branches**. Please don't ask us to pull from your master branch.
|
27
|
+
|
28
|
+
- **One pull request per feature**. If you want to do more than one thing, send
|
29
|
+
multiple pull requests.
|
30
|
+
|
31
|
+
- **Send coherent history**. Make sure each individual commit in your pull
|
32
|
+
request is meaningful. If you had to make multiple intermediate commits while
|
33
|
+
developing, please squash them before sending them to us.
|
data/README.md
CHANGED
@@ -8,6 +8,17 @@ Pundit provides a set of helpers which guide you in leveraging regular Ruby
|
|
8
8
|
classes and object oriented design patterns to build a simple, robust and
|
9
9
|
scaleable authorization system.
|
10
10
|
|
11
|
+
Links:
|
12
|
+
|
13
|
+
- [API documentation](http://www.rubydoc.info/gems/pundit)
|
14
|
+
- [Source Code](https://github.com/elabs/pundit)
|
15
|
+
- [Contributing](https://github.com/elabs/pundit/blob/master/CONTRIBUTING.md)
|
16
|
+
- [Code of Conduct](https://github.com/elabs/pundit/blob/master/CODE_OF_CONDUCT.md)
|
17
|
+
|
18
|
+
Sponsored by:
|
19
|
+
|
20
|
+
[<img src="http://d3cv91luii1z1d.cloudfront.net/logo-gh.png" alt="Elabs" height="50px"/>](http://elabs.se)
|
21
|
+
|
11
22
|
## Installation
|
12
23
|
|
13
24
|
``` ruby
|
@@ -75,11 +86,13 @@ generator, or set up your own base class to inherit from:
|
|
75
86
|
``` ruby
|
76
87
|
class PostPolicy < ApplicationPolicy
|
77
88
|
def update?
|
78
|
-
user.admin? or not
|
89
|
+
user.admin? or not record.published?
|
79
90
|
end
|
80
91
|
end
|
81
92
|
```
|
82
93
|
|
94
|
+
In the generated `ApplicationPolicy`, the model object is called `record`.
|
95
|
+
|
83
96
|
Supposing that you have an instance of class `Post`, Pundit now lets you do
|
84
97
|
this in your controller:
|
85
98
|
|
@@ -136,10 +149,14 @@ you can retrieve it by passing a symbol.
|
|
136
149
|
class DashboardPolicy < Struct.new(:user, :dashboard)
|
137
150
|
# ...
|
138
151
|
end
|
152
|
+
```
|
139
153
|
|
154
|
+
```ruby
|
140
155
|
# In controllers
|
141
156
|
authorize :dashboard, :show?
|
157
|
+
```
|
142
158
|
|
159
|
+
```erb
|
143
160
|
# In views
|
144
161
|
<% if policy(:dashboard).show? %>
|
145
162
|
<%= link_to 'Dashboard', dashboard_path %>
|
@@ -160,7 +177,7 @@ end
|
|
160
177
|
```
|
161
178
|
|
162
179
|
Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
|
163
|
-
will raise an exception in the vein of `verify_authorized`. However it tracks
|
180
|
+
will raise an exception in the vein of `verify_authorized`. However, it tracks
|
164
181
|
if `policy_scope` is used instead of `authorize`. This is mostly useful for
|
165
182
|
controller actions like `index` which find collections with a scope and don't
|
166
183
|
authorize individual instances.
|
@@ -171,6 +188,28 @@ class ApplicationController < ActionController::Base
|
|
171
188
|
end
|
172
189
|
```
|
173
190
|
|
191
|
+
If you're using `verify_authorized` in your controllers but need to
|
192
|
+
conditionally bypass verification, you can use `skip_authorization`. For
|
193
|
+
bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
|
194
|
+
in circumstances where you don't want to disable verification for the
|
195
|
+
entire action, but have some cases where you intend to not authorize.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
def show
|
199
|
+
record = Record.find_by(attribute: "value")
|
200
|
+
if record.present?
|
201
|
+
authorize record
|
202
|
+
else
|
203
|
+
skip_authorization
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
If you need to perform some more sophisticated logic or you want to raise a custom
|
209
|
+
exception you can use the two lower level methods `pundit_policy_authorized?`
|
210
|
+
and `pundit_policy_scoped?` which return `true` or `false` depending on whether
|
211
|
+
`authorize` or `policy_scope` have been called, respectively.
|
212
|
+
|
174
213
|
## Scopes
|
175
214
|
|
176
215
|
Often, you will want to have some kind of view listing records which a
|
@@ -335,13 +374,13 @@ class ApplicationController < ActionController::Base
|
|
335
374
|
private
|
336
375
|
|
337
376
|
def user_not_authorized
|
338
|
-
flash[:
|
377
|
+
flash[:alert] = "You are not authorized to perform this action."
|
339
378
|
redirect_to(request.referrer || root_path)
|
340
379
|
end
|
341
380
|
end
|
342
381
|
```
|
343
382
|
|
344
|
-
|
383
|
+
## Creating custom error messages
|
345
384
|
|
346
385
|
`NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
|
347
386
|
record (e.g. an instance of `Post`), and what policy (e.g. an instance of
|
@@ -360,8 +399,7 @@ class ApplicationController < ActionController::Base
|
|
360
399
|
def user_not_authorized(exception)
|
361
400
|
policy_name = exception.policy.class.to_s.underscore
|
362
401
|
|
363
|
-
flash[:error] =
|
364
|
-
default: 'You cannot perform this action.'
|
402
|
+
flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
|
365
403
|
redirect_to(request.referrer || root_path)
|
366
404
|
end
|
367
405
|
end
|
@@ -370,6 +408,7 @@ end
|
|
370
408
|
```yaml
|
371
409
|
en:
|
372
410
|
pundit:
|
411
|
+
default: 'You cannot perform this action.'
|
373
412
|
post_policy:
|
374
413
|
update?: 'You cannot edit this post!'
|
375
414
|
create?: 'You cannot create posts!'
|
@@ -408,14 +447,49 @@ def pundit_user
|
|
408
447
|
end
|
409
448
|
```
|
410
449
|
|
450
|
+
## Additional context
|
451
|
+
|
452
|
+
Pundit strongly encourages you to model your application in such a way that the
|
453
|
+
only context you need for authorization is a user object and a domain model that
|
454
|
+
you want to check authorization for. If you find yourself needing more context than
|
455
|
+
that, consider whether you are authorizing the right domain model, maybe another
|
456
|
+
domain model (or a wrapper around multiple domain models) can provide the context
|
457
|
+
you need.
|
458
|
+
|
459
|
+
Pundit does not allow you to pass additional arguments to policies for precisely
|
460
|
+
this reason.
|
461
|
+
|
462
|
+
However, in very rare cases, you might need to authorize based on more context than just
|
463
|
+
the currently authenticated user. Suppose for example that authorization is dependent
|
464
|
+
on IP address in addition to the authenticated user. In that case, one option is to
|
465
|
+
create a special class which wraps up both user and IP and passes it to the policy.
|
466
|
+
|
467
|
+
``` ruby
|
468
|
+
class UserContext
|
469
|
+
attr_reader :user, :ip
|
470
|
+
|
471
|
+
def initialize(user, ip)
|
472
|
+
@user = user
|
473
|
+
@ip = ip
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
class ApplicationController
|
478
|
+
include Pundit
|
479
|
+
|
480
|
+
def pundit_user
|
481
|
+
UserContext.new(current_user, request.ip)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
```
|
485
|
+
|
411
486
|
## Strong parameters
|
412
487
|
|
413
488
|
In Rails 4 (or Rails 3.2 with the
|
414
489
|
[strong_parameters](https://github.com/rails/strong_parameters) gem),
|
415
|
-
mass-assignment protection is handled in the controller.
|
416
|
-
|
417
|
-
|
418
|
-
permissions could be loaded.
|
490
|
+
mass-assignment protection is handled in the controller. With Pundit you can
|
491
|
+
control which attributes a user has access to update via your policies. You can
|
492
|
+
set up a `permitted_attributes` method in your policy like this:
|
419
493
|
|
420
494
|
```ruby
|
421
495
|
# app/policies/post_policy.rb
|
@@ -428,12 +502,16 @@ class PostPolicy < ApplicationPolicy
|
|
428
502
|
end
|
429
503
|
end
|
430
504
|
end
|
505
|
+
```
|
506
|
+
|
507
|
+
You can now retrieve these attributes from the policy:
|
431
508
|
|
509
|
+
```ruby
|
432
510
|
# app/controllers/posts_controller.rb
|
433
511
|
class PostsController < ApplicationController
|
434
512
|
def update
|
435
513
|
@post = Post.find(params[:id])
|
436
|
-
if @post.
|
514
|
+
if @post.update_attributes(post_params)
|
437
515
|
redirect_to @post
|
438
516
|
else
|
439
517
|
render :edit
|
@@ -443,7 +521,23 @@ class PostsController < ApplicationController
|
|
443
521
|
private
|
444
522
|
|
445
523
|
def post_params
|
446
|
-
params.require(:post).permit(
|
524
|
+
params.require(:post).permit(policy(@post).permitted_attributes)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
```
|
528
|
+
|
529
|
+
However, this is a bit cumbersome, so Pundit provides a convenient helper method:
|
530
|
+
|
531
|
+
```ruby
|
532
|
+
# app/controllers/posts_controller.rb
|
533
|
+
class PostsController < ApplicationController
|
534
|
+
def update
|
535
|
+
@post = Post.find(params[:id])
|
536
|
+
if @post.update_attributes(permitted_attributes(@post))
|
537
|
+
redirect_to @post
|
538
|
+
else
|
539
|
+
render :edit
|
540
|
+
end
|
447
541
|
end
|
448
542
|
end
|
449
543
|
```
|
@@ -463,7 +557,7 @@ Then put your policy specs in `spec/policies`, and make them look somewhat like
|
|
463
557
|
|
464
558
|
``` ruby
|
465
559
|
describe PostPolicy do
|
466
|
-
subject {
|
560
|
+
subject { described_class }
|
467
561
|
|
468
562
|
permissions :update? do
|
469
563
|
it "denies access if post is published" do
|
@@ -484,31 +578,6 @@ end
|
|
484
578
|
An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
|
485
579
|
[excellent post](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/).
|
486
580
|
|
487
|
-
### View Specs
|
488
|
-
|
489
|
-
When writing view specs, you'll notice that the policy helper is not available
|
490
|
-
and views under test that use it will fail. Thankfully, it's very easy to stub
|
491
|
-
out the policy to have it return whatever is appropriate for the spec.
|
492
|
-
|
493
|
-
``` ruby
|
494
|
-
describe "users/show" do
|
495
|
-
before(:each) do
|
496
|
-
user = assign(:user, build_stubbed(:user))
|
497
|
-
controller.stub(:current_user).and_return user
|
498
|
-
end
|
499
|
-
|
500
|
-
it "renders the destroy action" do
|
501
|
-
allow(view).to receive(:policy).and_return double(edit?: false, destroy?: true)
|
502
|
-
|
503
|
-
render
|
504
|
-
expect(rendered).to match 'Destroy'
|
505
|
-
end
|
506
|
-
end
|
507
|
-
```
|
508
|
-
|
509
|
-
This technique enables easy unit testing of tricky conditionaly view logic
|
510
|
-
based on what is or is not authorized.
|
511
|
-
|
512
581
|
# External Resources
|
513
582
|
|
514
583
|
- [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
|
@@ -1,20 +1,20 @@
|
|
1
|
-
require 'spec_helper'
|
1
|
+
require '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
|
2
2
|
|
3
3
|
describe <%= class_name %>Policy do
|
4
4
|
|
5
5
|
let(:user) { User.new }
|
6
6
|
|
7
|
-
subject {
|
7
|
+
subject { described_class }
|
8
8
|
|
9
9
|
permissions ".scope" do
|
10
10
|
pending "add some examples to (or delete) #{__FILE__}"
|
11
11
|
end
|
12
12
|
|
13
|
-
permissions :
|
13
|
+
permissions :show? do
|
14
14
|
pending "add some examples to (or delete) #{__FILE__}"
|
15
15
|
end
|
16
16
|
|
17
|
-
permissions :
|
17
|
+
permissions :create? do
|
18
18
|
pending "add some examples to (or delete) #{__FILE__}"
|
19
19
|
end
|
20
20
|
|
data/lib/pundit.rb
CHANGED
@@ -7,16 +7,39 @@ require "active_support/core_ext/module/introspection"
|
|
7
7
|
require "active_support/dependencies/autoload"
|
8
8
|
|
9
9
|
module Pundit
|
10
|
-
|
11
|
-
|
10
|
+
SUFFIX = "Policy"
|
11
|
+
|
12
|
+
class Error < StandardError; end
|
13
|
+
class NotAuthorizedError < Error
|
14
|
+
attr_reader :query, :record, :policy
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
@query = options[:query]
|
18
|
+
@record = options[:record]
|
19
|
+
@policy = options[:policy]
|
20
|
+
|
21
|
+
message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
|
22
|
+
|
23
|
+
super(message)
|
24
|
+
end
|
12
25
|
end
|
13
|
-
class AuthorizationNotPerformedError <
|
26
|
+
class AuthorizationNotPerformedError < Error; end
|
14
27
|
class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
|
15
|
-
class NotDefinedError <
|
28
|
+
class NotDefinedError < Error; end
|
16
29
|
|
17
30
|
extend ActiveSupport::Concern
|
18
31
|
|
19
32
|
class << self
|
33
|
+
def authorize(user, record, query)
|
34
|
+
policy = policy!(user, record)
|
35
|
+
|
36
|
+
unless policy.public_send(query)
|
37
|
+
raise NotAuthorizedError.new(query: query, record: record, policy: policy)
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
20
43
|
def policy_scope(user, scope)
|
21
44
|
policy_scope = PolicyFinder.new(scope).scope
|
22
45
|
policy_scope.new(user, scope).resolve if policy_scope
|
@@ -36,62 +59,100 @@ module Pundit
|
|
36
59
|
end
|
37
60
|
end
|
38
61
|
|
62
|
+
module Helper
|
63
|
+
def policy_scope(scope)
|
64
|
+
pundit_policy_scope(scope)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
39
68
|
included do
|
69
|
+
helper Helper if respond_to?(:helper)
|
40
70
|
if respond_to?(:helper_method)
|
41
|
-
helper_method :policy_scope
|
42
71
|
helper_method :policy
|
72
|
+
helper_method :pundit_policy_scope
|
43
73
|
helper_method :pundit_user
|
44
74
|
end
|
45
75
|
if respond_to?(:hide_action)
|
46
|
-
hide_action :policy_scope
|
47
|
-
hide_action :policy_scope=
|
48
76
|
hide_action :policy
|
49
|
-
hide_action :
|
77
|
+
hide_action :policy_scope
|
78
|
+
hide_action :policies
|
79
|
+
hide_action :policy_scopes
|
50
80
|
hide_action :authorize
|
51
81
|
hide_action :verify_authorized
|
52
82
|
hide_action :verify_policy_scoped
|
83
|
+
hide_action :permitted_attributes
|
53
84
|
hide_action :pundit_user
|
85
|
+
hide_action :skip_authorization
|
86
|
+
hide_action :skip_policy_scope
|
54
87
|
end
|
55
88
|
end
|
56
89
|
|
90
|
+
def pundit_policy_authorized?
|
91
|
+
!!@_pundit_policy_authorized
|
92
|
+
end
|
93
|
+
|
94
|
+
def pundit_policy_scoped?
|
95
|
+
!!@_pundit_policy_scoped
|
96
|
+
end
|
97
|
+
|
57
98
|
def verify_authorized
|
58
|
-
raise AuthorizationNotPerformedError unless
|
99
|
+
raise AuthorizationNotPerformedError unless pundit_policy_authorized?
|
59
100
|
end
|
60
101
|
|
61
102
|
def verify_policy_scoped
|
62
|
-
raise PolicyScopingNotPerformedError unless
|
103
|
+
raise PolicyScopingNotPerformedError unless pundit_policy_scoped?
|
63
104
|
end
|
64
105
|
|
65
106
|
def authorize(record, query=nil)
|
66
107
|
query ||= params[:action].to_s + "?"
|
67
|
-
|
108
|
+
|
109
|
+
@_pundit_policy_authorized = true
|
68
110
|
|
69
111
|
policy = policy(record)
|
70
112
|
unless policy.public_send(query)
|
71
|
-
|
72
|
-
error.query, error.record, error.policy = query, record, policy
|
73
|
-
|
74
|
-
raise error
|
113
|
+
raise NotAuthorizedError.new(query: query, record: record, policy: policy)
|
75
114
|
end
|
76
115
|
|
77
116
|
true
|
78
117
|
end
|
79
118
|
|
119
|
+
def skip_authorization
|
120
|
+
@_pundit_policy_authorized = true
|
121
|
+
end
|
122
|
+
|
123
|
+
def skip_policy_scope
|
124
|
+
@_pundit_policy_scoped = true
|
125
|
+
end
|
126
|
+
|
80
127
|
def policy_scope(scope)
|
81
|
-
@
|
82
|
-
|
128
|
+
@_pundit_policy_scoped = true
|
129
|
+
pundit_policy_scope(scope)
|
83
130
|
end
|
84
|
-
attr_writer :policy_scope
|
85
131
|
|
86
132
|
def policy(record)
|
87
|
-
|
133
|
+
policies[record] ||= Pundit.policy!(pundit_user, record)
|
88
134
|
end
|
89
135
|
|
90
|
-
def
|
91
|
-
|
136
|
+
def permitted_attributes(record)
|
137
|
+
name = record.class.to_s.demodulize.underscore
|
138
|
+
params.require(name).permit(policy(record).permitted_attributes)
|
139
|
+
end
|
140
|
+
|
141
|
+
def policies
|
142
|
+
@_pundit_policies ||= {}
|
143
|
+
end
|
144
|
+
|
145
|
+
def policy_scopes
|
146
|
+
@_pundit_policy_scopes ||= {}
|
92
147
|
end
|
93
148
|
|
94
149
|
def pundit_user
|
95
150
|
current_user
|
96
151
|
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def pundit_policy_scope(scope)
|
156
|
+
policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
|
157
|
+
end
|
97
158
|
end
|
data/lib/pundit/policy_finder.rb
CHANGED
@@ -21,17 +21,21 @@ module Pundit
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def scope!
|
24
|
-
|
24
|
+
raise NotDefinedError, "unable to find policy scope of nil" if object.nil?
|
25
|
+
scope or raise NotDefinedError, "unable to find scope `#{find}::Scope` for `#{object.inspect}`"
|
25
26
|
end
|
26
27
|
|
27
28
|
def policy!
|
28
|
-
|
29
|
+
raise NotDefinedError, "unable to find policy of nil" if object.nil?
|
30
|
+
policy or raise NotDefinedError, "unable to find policy `#{find}` for `#{object.inspect}`"
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
33
35
|
def find
|
34
|
-
if object.
|
36
|
+
if object.nil?
|
37
|
+
nil
|
38
|
+
elsif object.respond_to?(:policy_class)
|
35
39
|
object.policy_class
|
36
40
|
elsif object.class.respond_to?(:policy_class)
|
37
41
|
object.class.policy_class
|
@@ -44,10 +48,12 @@ module Pundit
|
|
44
48
|
object
|
45
49
|
elsif object.is_a?(Symbol)
|
46
50
|
object.to_s.classify
|
51
|
+
elsif object.is_a?(Array)
|
52
|
+
object.join('/').to_s.classify
|
47
53
|
else
|
48
54
|
object.class
|
49
55
|
end
|
50
|
-
"#{klass}
|
56
|
+
"#{klass}#{SUFFIX}"
|
51
57
|
end
|
52
58
|
end
|
53
59
|
end
|
data/lib/pundit/rspec.rb
CHANGED
@@ -7,19 +7,23 @@ module Pundit
|
|
7
7
|
|
8
8
|
matcher :permit do |user, record|
|
9
9
|
match_proc = lambda do |policy|
|
10
|
-
permissions.
|
10
|
+
@violating_permissions = permissions.find_all { |permission| not policy.new(user, record).public_send(permission) }
|
11
|
+
@violating_permissions.empty?
|
11
12
|
end
|
12
13
|
|
13
14
|
match_when_negated_proc = lambda do |policy|
|
14
|
-
permissions.
|
15
|
+
@violating_permissions = permissions.find_all { |permission| policy.new(user, record).public_send(permission) }
|
16
|
+
@violating_permissions.empty?
|
15
17
|
end
|
16
18
|
|
17
19
|
failure_message_proc = lambda do |policy|
|
18
|
-
|
20
|
+
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
21
|
+
"Expected #{policy} to grant #{permissions.to_sentence} on #{record} but #{@violating_permissions.to_sentence} #{was_were} not granted"
|
19
22
|
end
|
20
23
|
|
21
24
|
failure_message_when_negated_proc = lambda do |policy|
|
22
|
-
|
25
|
+
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
26
|
+
"Expected #{policy} not to grant #{permissions.to_sentence} on #{record} but #{@violating_permissions.to_sentence} #{was_were} granted"
|
23
27
|
end
|
24
28
|
|
25
29
|
if respond_to?(:match_when_negated)
|
data/lib/pundit/version.rb
CHANGED
data/pundit.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |gem|
|
|
20
20
|
|
21
21
|
gem.add_dependency "activesupport", ">= 3.0.0"
|
22
22
|
gem.add_development_dependency "activemodel", ">= 3.0.0"
|
23
|
+
gem.add_development_dependency "actionpack", ">= 3.0.0"
|
23
24
|
gem.add_development_dependency "bundler", "~> 1.3"
|
24
25
|
gem.add_development_dependency "rspec", ">=2.0.0"
|
25
26
|
gem.add_development_dependency "pry"
|
@@ -4,7 +4,7 @@ describe PostPolicy do
|
|
4
4
|
let(:user) { double }
|
5
5
|
let(:own_post) { double(user: user) }
|
6
6
|
let(:other_post) { double(user: double) }
|
7
|
-
subject {
|
7
|
+
subject { described_class }
|
8
8
|
|
9
9
|
permissions :update?, :show? do
|
10
10
|
it "is successful when all permissions match" do
|
data/spec/pundit_spec.rb
CHANGED
@@ -8,6 +8,27 @@ describe Pundit do
|
|
8
8
|
let(:controller) { Controller.new(user, { :action => 'update' }) }
|
9
9
|
let(:artificial_blog) { ArtificialBlog.new }
|
10
10
|
let(:article_tag) { ArticleTag.new }
|
11
|
+
let(:comments_relation) { CommentsRelation.new }
|
12
|
+
let(:empty_comments_relation) { CommentsRelation.new(true) }
|
13
|
+
|
14
|
+
describe ".authorize" do
|
15
|
+
it "infers the policy and authorizes based on it" do
|
16
|
+
expect(Pundit.authorize(user, post, :update?)).to be_truthy
|
17
|
+
end
|
18
|
+
|
19
|
+
it "works with anonymous class policies" do
|
20
|
+
expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy
|
21
|
+
expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises an error with a query and action" do
|
25
|
+
expect { Pundit.authorize(user, post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this #<Post>") do |error|
|
26
|
+
expect(error.query).to eq :destroy?
|
27
|
+
expect(error.record).to eq post
|
28
|
+
expect(error.policy).to eq Pundit.policy(user, post)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
11
32
|
|
12
33
|
describe ".policy_scope" do
|
13
34
|
it "returns an instantiated policy scope given a plain model class" do
|
@@ -18,9 +39,21 @@ describe Pundit do
|
|
18
39
|
expect(Pundit.policy_scope(user, Comment)).to eq Comment
|
19
40
|
end
|
20
41
|
|
42
|
+
it "returns an instantiated policy scope given an active record relation" do
|
43
|
+
expect(Pundit.policy_scope(user, comments_relation)).to eq comments_relation
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns an instantiated policy scope given an empty active record relation" do
|
47
|
+
expect(Pundit.policy_scope(user, empty_comments_relation)).to eq empty_comments_relation
|
48
|
+
end
|
49
|
+
|
21
50
|
it "returns nil if the given policy scope can't be found" do
|
22
51
|
expect(Pundit.policy_scope(user, Article)).to be_nil
|
23
52
|
end
|
53
|
+
|
54
|
+
it "returns nil if blank object given" do
|
55
|
+
expect(Pundit.policy_scope(user, nil)).to be_nil
|
56
|
+
end
|
24
57
|
end
|
25
58
|
|
26
59
|
describe ".policy_scope!" do
|
@@ -39,6 +72,10 @@ describe Pundit do
|
|
39
72
|
it "throws an exception if the given policy scope can't be found" do
|
40
73
|
expect { Pundit.policy_scope!(user, ArticleTag) }.to raise_error(Pundit::NotDefinedError)
|
41
74
|
end
|
75
|
+
|
76
|
+
it "throws an exception if the given policy scope is nil" do
|
77
|
+
expect { Pundit.policy_scope!(user, nil) }.to raise_error(Pundit::NotDefinedError, "unable to find policy scope of nil")
|
78
|
+
end
|
42
79
|
end
|
43
80
|
|
44
81
|
describe ".policy" do
|
@@ -71,6 +108,10 @@ describe Pundit do
|
|
71
108
|
expect(Pundit.policy(user, Article)).to be_nil
|
72
109
|
end
|
73
110
|
|
111
|
+
it "returns nil if the given policy is nil" do
|
112
|
+
expect(Pundit.policy(user, nil)).to be_nil
|
113
|
+
end
|
114
|
+
|
74
115
|
describe "with .policy_class set on the model" do
|
75
116
|
it "returns an instantiated policy given a plain model instance" do
|
76
117
|
policy = Pundit.policy(user, artificial_blog)
|
@@ -102,6 +143,13 @@ describe Pundit do
|
|
102
143
|
expect(policy.user).to eq user
|
103
144
|
expect(policy.dashboard).to eq :dashboard
|
104
145
|
end
|
146
|
+
|
147
|
+
it "returns an instantiated policy given an array" do
|
148
|
+
policy = Pundit.policy(user, [:project, :dashboard])
|
149
|
+
expect(policy.class).to eq Project::DashboardPolicy
|
150
|
+
expect(policy.user).to eq user
|
151
|
+
expect(policy.dashboard).to eq [:project, :dashboard]
|
152
|
+
end
|
105
153
|
end
|
106
154
|
end
|
107
155
|
|
@@ -137,10 +185,21 @@ describe Pundit do
|
|
137
185
|
expect(policy.dashboard).to eq :dashboard
|
138
186
|
end
|
139
187
|
|
188
|
+
it "returns an instantiated policy given an array" do
|
189
|
+
policy = Pundit.policy!(user, [:project, :dashboard])
|
190
|
+
expect(policy.class).to eq Project::DashboardPolicy
|
191
|
+
expect(policy.user).to eq user
|
192
|
+
expect(policy.dashboard).to eq [:project, :dashboard]
|
193
|
+
end
|
194
|
+
|
140
195
|
it "throws an exception if the given policy can't be found" do
|
141
196
|
expect { Pundit.policy!(user, article) }.to raise_error(Pundit::NotDefinedError)
|
142
197
|
expect { Pundit.policy!(user, Article) }.to raise_error(Pundit::NotDefinedError)
|
143
198
|
end
|
199
|
+
|
200
|
+
it "throws an exception if the given policy is nil" do
|
201
|
+
expect { Pundit.policy!(user, nil) }.to raise_error(Pundit::NotDefinedError, "unable to find policy of nil")
|
202
|
+
end
|
144
203
|
end
|
145
204
|
|
146
205
|
describe "#verify_authorized" do
|
@@ -165,8 +224,30 @@ describe Pundit do
|
|
165
224
|
end
|
166
225
|
end
|
167
226
|
|
227
|
+
describe "#pundit_policy_authorized?" do
|
228
|
+
it "is true when authorized" do
|
229
|
+
controller.authorize(post)
|
230
|
+
expect(controller.pundit_policy_authorized?).to be true
|
231
|
+
end
|
232
|
+
|
233
|
+
it "is false when not authorized" do
|
234
|
+
expect(controller.pundit_policy_authorized?).to be false
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "#pundit_policy_scoped?" do
|
239
|
+
it "is true when policy_scope is used" do
|
240
|
+
controller.policy_scope(Post)
|
241
|
+
expect(controller.pundit_policy_scoped?).to be true
|
242
|
+
end
|
243
|
+
|
244
|
+
it "is false when policy scope is not used" do
|
245
|
+
expect(controller.pundit_policy_scoped?).to be false
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
168
249
|
describe "#authorize" do
|
169
|
-
it "infers the policy name and
|
250
|
+
it "infers the policy name and authorizes based on it" do
|
170
251
|
expect(controller.authorize(post)).to be_truthy
|
171
252
|
end
|
172
253
|
|
@@ -180,16 +261,36 @@ describe Pundit do
|
|
180
261
|
expect { controller.authorize(article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
|
181
262
|
end
|
182
263
|
|
183
|
-
it "
|
264
|
+
it "throws an exception when the permission check fails" do
|
184
265
|
expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError)
|
185
266
|
end
|
186
267
|
|
187
|
-
it "
|
188
|
-
expect { controller.authorize(
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
268
|
+
it "throws an exception when a policy cannot be found" do
|
269
|
+
expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "caches the policy" do
|
273
|
+
expect(controller.policies[post]).to be_nil
|
274
|
+
controller.authorize(post)
|
275
|
+
expect(controller.policies[post]).not_to be_nil
|
276
|
+
end
|
277
|
+
|
278
|
+
it "raises an error when the given record is nil" do
|
279
|
+
expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotDefinedError)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "#skip_authorization" do
|
284
|
+
it "disables authorization verification" do
|
285
|
+
controller.skip_authorization
|
286
|
+
expect { controller.verify_authorized }.not_to raise_error
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe "#skip_policy_scope" do
|
291
|
+
it "disables policy scope verification" do
|
292
|
+
controller.skip_policy_scope
|
293
|
+
expect { controller.verify_policy_scoped }.not_to raise_error
|
193
294
|
end
|
194
295
|
end
|
195
296
|
|
@@ -199,7 +300,7 @@ describe Pundit do
|
|
199
300
|
end
|
200
301
|
end
|
201
302
|
|
202
|
-
describe "
|
303
|
+
describe "#policy" do
|
203
304
|
it "returns an instantiated policy" do
|
204
305
|
policy = controller.policy(post)
|
205
306
|
expect(policy.user).to eq user
|
@@ -212,13 +313,13 @@ describe Pundit do
|
|
212
313
|
|
213
314
|
it "allows policy to be injected" do
|
214
315
|
new_policy = OpenStruct.new
|
215
|
-
controller.
|
316
|
+
controller.policies[post] = new_policy
|
216
317
|
|
217
318
|
expect(controller.policy(post)).to eq new_policy
|
218
319
|
end
|
219
320
|
end
|
220
321
|
|
221
|
-
describe "
|
322
|
+
describe "#policy_scope" do
|
222
323
|
it "returns an instantiated policy scope" do
|
223
324
|
expect(controller.policy_scope(Post)).to eq :published
|
224
325
|
end
|
@@ -229,9 +330,18 @@ describe Pundit do
|
|
229
330
|
|
230
331
|
it "allows policy_scope to be injected" do
|
231
332
|
new_scope = OpenStruct.new
|
232
|
-
controller.
|
333
|
+
controller.policy_scopes[Post] = new_scope
|
334
|
+
|
335
|
+
expect(controller.policy_scope(Post)).to eq new_scope
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
describe "#permitted_attributes" do
|
340
|
+
it "checks policy for permitted attributes" do
|
341
|
+
params = ActionController::Parameters.new({ action: 'update', post: { title: 'Hello', votes: 5, admin: true } })
|
233
342
|
|
234
|
-
expect(
|
343
|
+
expect(Controller.new(user, params).permitted_attributes(post)).to eq({ 'title' => 'Hello', 'votes' => 5 })
|
344
|
+
expect(Controller.new(double, params).permitted_attributes(post)).to eq({ 'votes' => 5 })
|
235
345
|
end
|
236
346
|
end
|
237
347
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,19 +1,13 @@
|
|
1
|
-
require "
|
2
|
-
|
3
|
-
warnings = capture(:stderr) do
|
4
|
-
require "pundit"
|
5
|
-
require "pundit/rspec"
|
6
|
-
end
|
7
|
-
|
8
|
-
unless warnings.to_s.empty?
|
9
|
-
puts "ERROR: Encountered deprecation warning!"
|
10
|
-
puts warnings
|
11
|
-
exit 1
|
12
|
-
end
|
1
|
+
require "pundit"
|
2
|
+
require "pundit/rspec"
|
13
3
|
|
4
|
+
require "rack"
|
5
|
+
require "rack/test"
|
14
6
|
require "pry"
|
7
|
+
require "active_support"
|
15
8
|
require "active_support/core_ext"
|
16
9
|
require "active_model/naming"
|
10
|
+
require "action_controller/metal/strong_parameters"
|
17
11
|
|
18
12
|
I18n.enforce_available_locales = false
|
19
13
|
|
@@ -41,6 +35,13 @@ class PostPolicy < Struct.new(:user, :post)
|
|
41
35
|
def show?
|
42
36
|
true
|
43
37
|
end
|
38
|
+
def permitted_attributes
|
39
|
+
if post.user == user
|
40
|
+
[:title, :votes]
|
41
|
+
else
|
42
|
+
[:votes]
|
43
|
+
end
|
44
|
+
end
|
44
45
|
end
|
45
46
|
class PostPolicy::Scope < Struct.new(:user, :scope)
|
46
47
|
def resolve
|
@@ -51,6 +52,8 @@ class Post < Struct.new(:user)
|
|
51
52
|
def self.published
|
52
53
|
:published
|
53
54
|
end
|
55
|
+
def to_s; "Post"; end
|
56
|
+
def inspect; "#<Post>"; end
|
54
57
|
end
|
55
58
|
|
56
59
|
class CommentPolicy < Struct.new(:user, :comment); end
|
@@ -61,6 +64,13 @@ class CommentPolicy::Scope < Struct.new(:user, :scope)
|
|
61
64
|
end
|
62
65
|
class Comment; extend ActiveModel::Naming; end
|
63
66
|
|
67
|
+
# minimum mock for an ActiveRecord Relation returning comments
|
68
|
+
class CommentsRelation
|
69
|
+
def initialize(empty=false); @empty=empty; end
|
70
|
+
def blank?; @empty; end
|
71
|
+
def model_name; Comment.model_name; end
|
72
|
+
end
|
73
|
+
|
64
74
|
class Article; end
|
65
75
|
|
66
76
|
class BlogPolicy < Struct.new(:user, :blog); end
|
@@ -85,6 +95,16 @@ end
|
|
85
95
|
|
86
96
|
class DashboardPolicy < Struct.new(:user, :dashboard); end
|
87
97
|
|
98
|
+
module Project
|
99
|
+
class DashboardPolicy < Struct.new(:user, :dashboard); end
|
100
|
+
end
|
101
|
+
|
102
|
+
class DenierPolicy < Struct.new(:user, :record)
|
103
|
+
def update?
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
88
108
|
class Controller
|
89
109
|
include Pundit
|
90
110
|
|
@@ -95,3 +115,15 @@ class Controller
|
|
95
115
|
@params = params
|
96
116
|
end
|
97
117
|
end
|
118
|
+
|
119
|
+
class NilClassPolicy
|
120
|
+
class Scope
|
121
|
+
def initialize(*)
|
122
|
+
raise "I'm only here to be annoying!"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def initialize(*)
|
127
|
+
raise "I'm only here to be annoying!"
|
128
|
+
end
|
129
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pundit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonas Nicklas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2015-04-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -39,6 +39,20 @@ dependencies:
|
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: 3.0.0
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: actionpack
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 3.0.0
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 3.0.0
|
42
56
|
- !ruby/object:Gem::Dependency
|
43
57
|
name: bundler
|
44
58
|
requirement: !ruby/object:Gem::Requirement
|
@@ -120,6 +134,8 @@ files:
|
|
120
134
|
- ".gitignore"
|
121
135
|
- ".travis.yml"
|
122
136
|
- CHANGELOG.md
|
137
|
+
- CODE_OF_CONDUCT.md
|
138
|
+
- CONTRIBUTING.md
|
123
139
|
- Gemfile
|
124
140
|
- LICENSE.txt
|
125
141
|
- README.md
|
@@ -162,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
178
|
version: '0'
|
163
179
|
requirements: []
|
164
180
|
rubyforge_project:
|
165
|
-
rubygems_version: 2.
|
181
|
+
rubygems_version: 2.4.5
|
166
182
|
signing_key:
|
167
183
|
specification_version: 4
|
168
184
|
summary: OO authorization for Rails
|