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 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