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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dce73a23b63776e71c55792ade6b1066fb5a5896
4
- data.tar.gz: b4bdb26c70cb66aa77b1424dd2fe4cd1bc340926
3
+ metadata.gz: b2e10a94ed9f8cbd38d1c51f38344eedd7d680d8
4
+ data.tar.gz: 89b88467b3f85513d764ff7f2357f98dfa078b5c
5
5
  SHA512:
6
- metadata.gz: 76b5740261dd485d461e2209c1fe5558faf2eda1917cdb2f87c4fe7eff026d1519341c260f2f071b37384516a886d6f1cafebdaf7ef688c3c908e59fce028cc8
7
- data.tar.gz: d9c8e46eb64b1a03f4741db7bbede7781180f41bd63de8291ab1ebd133c2a9108ff4e0f22d6ba0b41a290325f318281fc767df2224ff2a4f5013da2f77071c6f
6
+ metadata.gz: 78331e942eabf7eb0b0990e0760481498cd056f10f3b11ecc7f9fb9718ae6532a1bd64a3682d012d8245ab019cf39f2e8671dff82e55dee1bf4e76afcf62dd05
7
+ data.tar.gz: c95f7e2fb27998009aeb6948464ce550938fc59a21d9169d0def6df7b3e467140f2d00fa198b97adce8cb08d232bd42d95cda46ab84813e08f305cc2277aee40
@@ -1,11 +1,11 @@
1
1
  language: ruby
2
+ sudo: false
2
3
  rvm:
3
- - 1.9.3
4
4
  - 2.0.0
5
- - 2.1.0
5
+ - 2.1.5
6
+ - 2.2.0
6
7
  - jruby-19mode
7
8
  - rbx-2
8
9
  env:
9
10
  - RSPEC_VERSION="<2.99"
10
- - RSPEC_VERSION="~>2.99"
11
11
  - RSPEC_VERSION="~>3.0
@@ -1,6 +1,21 @@
1
1
  # Pundit
2
2
 
3
- ## 0.3.0 (unreleased)
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)
@@ -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/)
@@ -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 post.published?
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[:error] = "You are not authorized to perform this action."
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
- ### Creating custom error messages
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] = I18n.t "pundit.#{policy_name}.#{exception.query}",
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
- Pundit helps you permit different users to set different attributes. Don't
417
- forget to provide your policy an instance of object or a class so correct
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.update(post_params)
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(*policy(@post || Post).permitted_attributes)
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 { PostPolicy }
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)
@@ -51,4 +51,3 @@ class ApplicationPolicy
51
51
  end
52
52
  end
53
53
  end
54
-
@@ -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 { <%= class_name %>Policy }
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 :create? do
13
+ permissions :show? do
14
14
  pending "add some examples to (or delete) #{__FILE__}"
15
15
  end
16
16
 
17
- permissions :show? do
17
+ permissions :create? do
18
18
  pending "add some examples to (or delete) #{__FILE__}"
19
19
  end
20
20
 
@@ -5,10 +5,10 @@ class <%= class_name %>PolicyTest < ActiveSupport::TestCase
5
5
  def test_scope
6
6
  end
7
7
 
8
- def test_create
8
+ def test_show
9
9
  end
10
10
 
11
- def test_show
11
+ def test_create
12
12
  end
13
13
 
14
14
  def test_update
@@ -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
- class NotAuthorizedError < StandardError
11
- attr_accessor :query, :record, :policy
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 < StandardError; end
26
+ class AuthorizationNotPerformedError < Error; end
14
27
  class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
15
- class NotDefinedError < StandardError; end
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 :policy=
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 @_policy_authorized
99
+ raise AuthorizationNotPerformedError unless pundit_policy_authorized?
59
100
  end
60
101
 
61
102
  def verify_policy_scoped
62
- raise PolicyScopingNotPerformedError unless @_policy_scoped
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
- @_policy_authorized = true
108
+
109
+ @_pundit_policy_authorized = true
68
110
 
69
111
  policy = policy(record)
70
112
  unless policy.public_send(query)
71
- error = NotAuthorizedError.new("not allowed to #{query} this #{record}")
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
- @_policy_scoped = true
82
- @policy_scope or Pundit.policy_scope!(pundit_user, scope)
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
- @_policy or Pundit.policy!(pundit_user, record)
133
+ policies[record] ||= Pundit.policy!(pundit_user, record)
88
134
  end
89
135
 
90
- def policy=(policy)
91
- @_policy = policy
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
@@ -21,17 +21,21 @@ module Pundit
21
21
  end
22
22
 
23
23
  def scope!
24
- scope or raise NotDefinedError, "unable to find scope #{find}::Scope for #{object}"
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
- policy or raise NotDefinedError, "unable to find policy #{find} for #{object}"
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.respond_to?(:policy_class)
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}Policy"
56
+ "#{klass}#{SUFFIX}"
51
57
  end
52
58
  end
53
59
  end
@@ -7,19 +7,23 @@ module Pundit
7
7
 
8
8
  matcher :permit do |user, record|
9
9
  match_proc = lambda do |policy|
10
- permissions.all? { |permission| policy.new(user, record).public_send(permission) }
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.none? { |permission| policy.new(user, record).public_send(permission) }
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
- "Expected #{policy} to grant #{permissions.to_sentence} on #{record} but it didn't"
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
- "Expected #{policy} not to grant #{permissions.to_sentence} on #{record} but it did"
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)
@@ -1,3 +1,3 @@
1
1
  module Pundit
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -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 { PostPolicy }
7
+ subject { described_class }
8
8
 
9
9
  permissions :update?, :show? do
10
10
  it "is successful when all permissions match" do
@@ -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 authorized based on it" do
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 "raises an error when the permission check fails" do
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 "raises an error with a query and action" do
188
- expect { controller.authorize(post, :destroy?) }.to raise_error do |error|
189
- expect(error.query).to eq :destroy?
190
- expect(error.record).to eq post
191
- expect(error.policy).to eq controller.policy(post)
192
- end
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 ".policy" do
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.policy = new_policy
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 ".policy_scope" do
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.policy_scope = new_scope
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(controller.policy_scope(post)).to eq new_scope
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
@@ -1,19 +1,13 @@
1
- require "active_support/core_ext/kernel"
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.3.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: 2014-08-22 00:00:00.000000000 Z
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.2.2
181
+ rubygems_version: 2.4.5
166
182
  signing_key:
167
183
  specification_version: 4
168
184
  summary: OO authorization for Rails