pundit 0.3.0 → 1.0.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/.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
|