Varvet
20 |
21 | ## Installation
22 |
23 | > **Please note** that the README on GitHub is accurate with the _latest code on GitHub_. You are most likely using a released version of Pundit, so please refer to the [documentation for the latest released version of Pundit](https://www.rubydoc.info/gems/pundit).
24 |
25 | ``` sh
26 | bundle add pundit
27 | ```
28 |
29 | Include `Pundit::Authorization` in your application controller:
30 |
31 | ``` ruby
32 | class ApplicationController < ActionController::Base
33 | include Pundit::Authorization
34 | end
35 | ```
36 |
37 | Optionally, you can run the generator, which will set up an application policy
38 | with some useful defaults for you:
39 |
40 | ``` sh
41 | rails g pundit:install
42 | ```
43 |
44 | After generating your application policy, restart the Rails server so that Rails
45 | can pick up any classes in the new `app/policies/` directory.
46 |
47 | ## Policies
48 |
49 | Pundit is focused around the notion of policy classes. We suggest that you put
50 | these classes in `app/policies`. This is an example that allows updating a post
51 | if the user is an admin, or if the post is unpublished:
52 |
53 | ``` ruby
54 | class PostPolicy
55 | attr_reader :user, :post
56 |
57 | def initialize(user, post)
58 | @user = user
59 | @post = post
60 | end
61 |
62 | def update?
63 | user.admin? || !post.published?
64 | end
65 | end
66 | ```
67 |
68 | As you can see, this is a plain Ruby class. Pundit makes the following
69 | assumptions about this class:
70 |
71 | - The class has the same name as some kind of model class, only suffixed
72 | with the word "Policy".
73 | - The first argument is a user. In your controller, Pundit will call the
74 | `current_user` method to retrieve what to send into this argument
75 | - The second argument is some kind of model object, whose authorization
76 | you want to check. This does not need to be an ActiveRecord or even
77 | an ActiveModel object, it can be anything really.
78 | - The class implements some kind of query method, in this case `update?`.
79 | Usually, this will map to the name of a particular controller action.
80 |
81 | That's it really.
82 |
83 | Usually you'll want to inherit from the application policy created by the
84 | generator, or set up your own base class to inherit from:
85 |
86 | ``` ruby
87 | class PostPolicy < ApplicationPolicy
88 | def update?
89 | user.admin? or not record.published?
90 | end
91 | end
92 | ```
93 |
94 | In the generated `ApplicationPolicy`, the model object is called `record`.
95 |
96 | Supposing that you have an instance of class `Post`, Pundit now lets you do
97 | this in your controller:
98 |
99 | ``` ruby
100 | def update
101 | @post = Post.find(params[:id])
102 | authorize @post
103 | if @post.update(post_params)
104 | redirect_to @post
105 | else
106 | render :edit
107 | end
108 | end
109 | ```
110 |
111 | The authorize method automatically infers that `Post` will have a matching
112 | `PostPolicy` class, and instantiates this class, handing in the current user
113 | and the given record. It then infers from the action name, that it should call
114 | `update?` on this instance of the policy. In this case, you can imagine that
115 | `authorize` would have done something like this:
116 |
117 | ``` ruby
118 | unless PostPolicy.new(current_user, @post).update?
119 | raise Pundit::NotAuthorizedError, "not allowed to PostPolicy#update? this Post"
120 | end
121 | ```
122 |
123 | You can pass a second argument to `authorize` if the name of the permission you
124 | want to check doesn't match the action name. For example:
125 |
126 | ``` ruby
127 | def publish
128 | @post = Post.find(params[:id])
129 | authorize @post, :update?
130 | @post.publish!
131 | redirect_to @post
132 | end
133 | ```
134 |
135 | You can pass an argument to override the policy class if necessary. For example:
136 |
137 | ```ruby
138 | def create
139 | @publication = find_publication # assume this method returns any model that behaves like a publication
140 | # @publication.class => Post
141 | authorize @publication, policy_class: PublicationPolicy
142 | @publication.publish!
143 | redirect_to @publication
144 | end
145 | ```
146 |
147 | If you don't have an instance for the first argument to `authorize`, then you can pass
148 | the class. For example:
149 |
150 | Policy:
151 | ```ruby
152 | class PostPolicy < ApplicationPolicy
153 | def admin_list?
154 | user.admin?
155 | end
156 | end
157 | ```
158 |
159 | Controller:
160 | ```ruby
161 | def admin_list
162 | authorize Post # we don't have a particular post to authorize
163 | # Rest of controller action
164 | end
165 | ```
166 |
167 | `authorize` returns the instance passed to it, so you can chain it like this:
168 |
169 | Controller:
170 | ```ruby
171 | def show
172 | @user = authorize User.find(params[:id])
173 | end
174 |
175 | # return the record even for namespaced policies
176 | def show
177 | @user = authorize [:admin, User.find(params[:id])]
178 | end
179 | ```
180 |
181 | You can easily get a hold of an instance of the policy through the `policy`
182 | method in both the view and controller. This is especially useful for
183 | conditionally showing links or buttons in the view:
184 |
185 | ``` erb
186 | <% if policy(@post).update? %>
187 | <%= link_to "Edit post", edit_post_path(@post) %>
188 | <% end %>
189 | ```
190 | ## Headless policies
191 |
192 | Given there is a policy without a corresponding model / ruby class,
193 | you can retrieve it by passing a symbol.
194 |
195 | ```ruby
196 | # app/policies/dashboard_policy.rb
197 | class DashboardPolicy
198 | attr_reader :user
199 |
200 | # `_record` in this example will be :dashboard
201 | def initialize(user, _record)
202 | @user = user
203 | end
204 |
205 | def show?
206 | user.admin?
207 | end
208 | end
209 | ```
210 |
211 | Note that the headless policy still needs to accept two arguments. The
212 | second argument will be the symbol `:dashboard` in this case, which
213 | is what is passed as the record to `authorize` below.
214 |
215 | ```ruby
216 | # In controllers
217 | def show
218 | authorize :dashboard, :show?
219 | ...
220 | end
221 | ```
222 |
223 | ```erb
224 | # In views
225 | <% if policy(:dashboard).show? %>
226 | <%= link_to 'Dashboard', dashboard_path %>
227 | <% end %>
228 | ```
229 |
230 | ## Scopes
231 |
232 | Often, you will want to have some kind of view listing records which a
233 | particular user has access to. When using Pundit, you are expected to
234 | define a class called a policy scope. It can look something like this:
235 |
236 | ``` ruby
237 | class PostPolicy < ApplicationPolicy
238 | class Scope
239 | def initialize(user, scope)
240 | @user = user
241 | @scope = scope
242 | end
243 |
244 | def resolve
245 | if user.admin?
246 | scope.all
247 | else
248 | scope.where(published: true)
249 | end
250 | end
251 |
252 | private
253 |
254 | attr_reader :user, :scope
255 | end
256 |
257 | def update?
258 | user.admin? or not record.published?
259 | end
260 | end
261 | ```
262 |
263 | Pundit makes the following assumptions about this class:
264 |
265 | - The class has the name `Scope` and is nested under the policy class.
266 | - The first argument is a user. In your controller, Pundit will call the
267 | `current_user` method to retrieve what to send into this argument.
268 | - The second argument is a scope of some kind on which to perform some kind of
269 | query. It will usually be an ActiveRecord class or a
270 | `ActiveRecord::Relation`, but it could be something else entirely.
271 | - Instances of this class respond to the method `resolve`, which should return
272 | some kind of result which can be iterated over. For ActiveRecord classes,
273 | this would usually be an `ActiveRecord::Relation`.
274 |
275 | You'll probably want to inherit from the application policy scope generated by the
276 | generator, or create your own base class to inherit from:
277 |
278 | ``` ruby
279 | class PostPolicy < ApplicationPolicy
280 | class Scope < ApplicationPolicy::Scope
281 | def resolve
282 | if user.admin?
283 | scope.all
284 | else
285 | scope.where(published: true)
286 | end
287 | end
288 | end
289 |
290 | def update?
291 | user.admin? or not record.published?
292 | end
293 | end
294 | ```
295 |
296 | You can now use this class from your controller via the `policy_scope` method:
297 |
298 | ``` ruby
299 | def index
300 | @posts = policy_scope(Post)
301 | end
302 |
303 | def show
304 | @post = policy_scope(Post).find(params[:id])
305 | end
306 | ```
307 |
308 | Like with the authorize method, you can also override the policy scope class:
309 |
310 | ``` ruby
311 | def index
312 | # publication_class => Post
313 | @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
314 | end
315 | ```
316 |
317 | In this case it is a shortcut for doing:
318 |
319 | ``` ruby
320 | def index
321 | @publications = PublicationPolicy::Scope.new(current_user, Post).resolve
322 | end
323 | ```
324 |
325 | You can, and are encouraged to, use this method in views:
326 |
327 | ``` erb
328 | <% policy_scope(@user.posts).each do |post| %>
329 | <%= link_to post.title, post_path(post) %>
330 | <% end %>
331 | ```
332 |
333 | ## Ensuring policies and scopes are used
334 |
335 | When you are developing an application with Pundit it can be easy to forget to
336 | authorize some action. People are forgetful after all. Since Pundit encourages
337 | you to add the `authorize` call manually to each controller action, it's really
338 | easy to miss one.
339 |
340 | Thankfully, Pundit has a handy feature which reminds you in case you forget.
341 | Pundit tracks whether you have called `authorize` anywhere in your controller
342 | action. Pundit also adds a method to your controllers called
343 | `verify_authorized`. This method will raise an exception if `authorize` has not
344 | yet been called. You should run this method in an `after_action` hook to ensure
345 | that you haven't forgotten to authorize the action. For example:
346 |
347 | ``` ruby
348 | class ApplicationController < ActionController::Base
349 | include Pundit::Authorization
350 | after_action :verify_authorized
351 | end
352 | ```
353 |
354 | Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
355 | will raise an exception similar to `verify_authorized`. However, it tracks
356 | if `policy_scope` is used instead of `authorize`. This is mostly useful for
357 | controller actions like `index` which find collections with a scope and don't
358 | authorize individual instances.
359 |
360 | ``` ruby
361 | class ApplicationController < ActionController::Base
362 | include Pundit::Authorization
363 | after_action :verify_pundit_authorization
364 |
365 | def verify_pundit_authorization
366 | if action_name == "index"
367 | verify_policy_scoped
368 | else
369 | verify_authorized
370 | end
371 | end
372 | end
373 | ```
374 |
375 | **This verification mechanism only exists to aid you while developing your
376 | application, so you don't forget to call `authorize`. It is not some kind of
377 | failsafe mechanism or authorization mechanism. You should be able to remove
378 | these filters without affecting how your app works in any way.**
379 |
380 | Some people have found this feature confusing, while many others
381 | find it extremely helpful. If you fall into the category of people who find it
382 | confusing then you do not need to use it. Pundit will work fine without
383 | using `verify_authorized` and `verify_policy_scoped`.
384 |
385 | ### Conditional verification
386 |
387 | If you're using `verify_authorized` in your controllers but need to
388 | conditionally bypass verification, you can use `skip_authorization`. For
389 | bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
390 | in circumstances where you don't want to disable verification for the
391 | entire action, but have some cases where you intend to not authorize.
392 |
393 | ```ruby
394 | def show
395 | record = Record.find_by(attribute: "value")
396 | if record.present?
397 | authorize record
398 | else
399 | skip_authorization
400 | end
401 | end
402 | ```
403 |
404 | ## Manually specifying policy classes
405 |
406 | Sometimes you might want to explicitly declare which policy to use for a given
407 | class, instead of letting Pundit infer it. This can be done like so:
408 |
409 | ``` ruby
410 | class Post
411 | def self.policy_class
412 | PostablePolicy
413 | end
414 | end
415 | ```
416 |
417 | Alternatively, you can declare an instance method:
418 |
419 | ``` ruby
420 | class Post
421 | def policy_class
422 | PostablePolicy
423 | end
424 | end
425 | ```
426 |
427 | ## Plain old Ruby
428 |
429 | Pundit is a very small library on purpose, and it doesn't do anything you can't do yourself. There's no secret sauce here. It does as little as possible, and then gets out of your way.
430 |
431 | With the few but powerful helpers available in Pundit, you have the power to build a well structured, fully working authorization system without using any special DSLs or funky syntax.
432 |
433 | Remember that all of the policy and scope classes are plain Ruby classes, which means you can use the same mechanisms you always use to DRY things up. Encapsulate a set of permissions into a module and include them in multiple policies. Use `alias_method` to make some permissions behave the same as others. Inherit from a base set of permissions. Use metaprogramming if you really have to.
434 |
435 | ## Generator
436 |
437 | Use the supplied generator to generate policies:
438 |
439 | ``` sh
440 | rails g pundit:policy post
441 | ```
442 |
443 | ## Closed systems
444 |
445 | In many applications, only logged in users are really able to do anything. If
446 | you're building such a system, it can be kind of cumbersome to check that the
447 | user in a policy isn't `nil` for every single permission. Aside from policies,
448 | you can add this check to the base class for scopes.
449 |
450 | We suggest that you define a filter that redirects unauthenticated users to the
451 | login page. As a secondary defence, if you've defined an ApplicationPolicy, it
452 | might be a good idea to raise an exception if somehow an unauthenticated user
453 | got through. This way you can fail more gracefully.
454 |
455 | ``` ruby
456 | class ApplicationPolicy
457 | def initialize(user, record)
458 | raise Pundit::NotAuthorizedError, "must be logged in" unless user
459 | @user = user
460 | @record = record
461 | end
462 |
463 | class Scope
464 | attr_reader :user, :scope
465 |
466 | def initialize(user, scope)
467 | raise Pundit::NotAuthorizedError, "must be logged in" unless user
468 | @user = user
469 | @scope = scope
470 | end
471 | end
472 | end
473 | ```
474 |
475 | ## NilClassPolicy
476 |
477 | To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern)
478 | you may find that you want to implement a `NilClassPolicy`. This might be useful
479 | where you want to extend your ApplicationPolicy to allow some tolerance of, for
480 | example, associations which might be `nil`.
481 |
482 | ```ruby
483 | class NilClassPolicy < ApplicationPolicy
484 | class Scope < ApplicationPolicy::Scope
485 | def resolve
486 | raise Pundit::NotDefinedError, "Cannot scope NilClass"
487 | end
488 | end
489 |
490 | def show?
491 | false # Nobody can see nothing
492 | end
493 | end
494 | ```
495 |
496 | ## Rescuing a denied Authorization in Rails
497 |
498 | Pundit raises a `Pundit::NotAuthorizedError` you can
499 | [rescue_from](https://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
500 | in your `ApplicationController`. You can customize the `user_not_authorized`
501 | method in every controller.
502 |
503 | ```ruby
504 | class ApplicationController < ActionController::Base
505 | include Pundit::Authorization
506 |
507 | rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
508 |
509 | private
510 |
511 | def user_not_authorized
512 | flash[:alert] = "You are not authorized to perform this action."
513 | redirect_back_or_to(root_path)
514 | end
515 | end
516 | ```
517 |
518 | Alternatively, you can globally handle Pundit::NotAuthorizedError's by having rails handle them as a 403 error and serving a 403 error page. Add the following to application.rb:
519 |
520 | ```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden```
521 |
522 | ## Creating custom error messages
523 |
524 | `NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
525 | record (e.g. an instance of `Post`), and what policy (e.g. an instance of
526 | `PostPolicy`) caused the error to be raised.
527 |
528 | One way to use these `query`, `record`, and `policy` properties is to connect
529 | them with `I18n` to generate error messages. Here's how you might go about doing
530 | that.
531 |
532 | ```ruby
533 | class ApplicationController < ActionController::Base
534 | rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
535 |
536 | private
537 |
538 | def user_not_authorized(exception)
539 | policy_name = exception.policy.class.to_s.underscore
540 |
541 | flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
542 | redirect_back_or_to(root_path)
543 | end
544 | end
545 | ```
546 |
547 | ```yaml
548 | en:
549 | pundit:
550 | default: 'You cannot perform this action.'
551 | post_policy:
552 | update?: 'You cannot edit this post!'
553 | create?: 'You cannot create posts!'
554 | ```
555 |
556 | This is an example. Pundit is agnostic as to how you implement your error messaging.
557 |
558 | ## Manually retrieving policies and scopes
559 |
560 | Sometimes you want to retrieve a policy for a record outside the controller or
561 | view. For example when you delegate permissions from one policy to another.
562 |
563 | You can easily retrieve policies and scopes like this:
564 |
565 | ``` ruby
566 | Pundit.policy!(user, post)
567 | Pundit.policy(user, post)
568 |
569 | Pundit.policy_scope!(user, Post)
570 | Pundit.policy_scope(user, Post)
571 | ```
572 |
573 | The bang methods will raise an exception if the policy does not exist, whereas
574 | those without the bang will return nil.
575 |
576 | ## Customize Pundit user
577 |
578 | On occasion, your controller may be unable to access `current_user`, or the method that should be invoked by Pundit may not be `current_user`. To address this, you can define a method in your controller named `pundit_user`.
579 |
580 | ```ruby
581 | def pundit_user
582 | User.find_by_other_means
583 | end
584 | ```
585 |
586 | For instance, Rails 8 includes a built-in [authentication generator](https://github.com/rails/rails/tree/8-0-stable/railties/lib/rails/generators/rails/authentication). If you choose to use it, the currently logged-in user is accessed via `Current.user` instead of `current_user`.
587 |
588 | To ensure compatibility with Pundit, define a `pundit_user` method in `application_controller.rb` (or another suitable location) as follows:
589 |
590 | ```ruby
591 | def pundit_user
592 | Current.user
593 | end
594 | ```
595 |
596 | ### Handling User Switching in Pundit
597 |
598 | When switching users in your application, it's important to reset the Pundit user context to ensure that authorization policies are applied correctly for the new user. Pundit caches the user context, so failing to reset it could result in incorrect permissions being applied.
599 |
600 | To handle user switching, you can use the following pattern in your controller:
601 |
602 | ```ruby
603 | class ApplicationController
604 | include Pundit::Authorization
605 |
606 | def switch_user_to(user)
607 | terminate_session if authenticated?
608 | start_new_session_for user
609 | pundit_reset!
610 | end
611 | end
612 | ```
613 |
614 | Make sure to invoke `pundit_reset!` whenever changing the user. This ensures the cached authorization context is reset, preventing any incorrect permissions from being applied.
615 |
616 | ## Policy Namespacing
617 | In some cases it might be helpful to have multiple policies that serve different contexts for a
618 | resource. A prime example of this is the case where User policies differ from Admin policies. To
619 | authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array:
620 |
621 | ```ruby
622 | authorize(post) # => will look for a PostPolicy
623 | authorize([:admin, post]) # => will look for an Admin::PostPolicy
624 | authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy
625 |
626 | policy_scope(Post) # => will look for a PostPolicy::Scope
627 | policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope
628 | policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope
629 | ```
630 |
631 | If you are using namespaced policies for something like Admin views, it can be useful to
632 | override the `policy_scope` and `authorize` helpers in your `AdminController` to automatically
633 | apply the namespacing:
634 |
635 | ```ruby
636 | class AdminController < ApplicationController
637 | def policy_scope(scope)
638 | super([:admin, scope])
639 | end
640 |
641 | def authorize(record, query = nil)
642 | super([:admin, record], query)
643 | end
644 | end
645 |
646 | class Admin::PostController < AdminController
647 | def index
648 | policy_scope(Post)
649 | end
650 |
651 | def show
652 | post = authorize Post.find(params[:id])
653 | end
654 | end
655 | ```
656 |
657 | ## Additional context
658 |
659 | Pundit strongly encourages you to model your application in such a way that the
660 | only context you need for authorization is a user object and a domain model that
661 | you want to check authorization for. If you find yourself needing more context than
662 | that, consider whether you are authorizing the right domain model, maybe another
663 | domain model (or a wrapper around multiple domain models) can provide the context
664 | you need.
665 |
666 | Pundit does not allow you to pass additional arguments to policies for precisely
667 | this reason.
668 |
669 | However, in very rare cases, you might need to authorize based on more context than just
670 | the currently authenticated user. Suppose for example that authorization is dependent
671 | on IP address in addition to the authenticated user. In that case, one option is to
672 | create a special class which wraps up both user and IP and passes it to the policy.
673 |
674 | ``` ruby
675 | class UserContext
676 | attr_reader :user, :ip
677 |
678 | def initialize(user, ip)
679 | @user = user
680 | @ip = ip
681 | end
682 | end
683 |
684 | class ApplicationController
685 | include Pundit::Authorization
686 |
687 | def pundit_user
688 | UserContext.new(current_user, request.ip)
689 | end
690 | end
691 | ```
692 |
693 | ## Strong parameters
694 |
695 | In Rails,
696 | mass-assignment protection is handled in the controller. With Pundit you can
697 | control which attributes a user has access to update via your policies. You can
698 | set up a `permitted_attributes` method in your policy like this:
699 |
700 | ```ruby
701 | # app/policies/post_policy.rb
702 | class PostPolicy < ApplicationPolicy
703 | def permitted_attributes
704 | if user.admin? || user.owner_of?(post)
705 | [:title, :body, :tag_list]
706 | else
707 | [:tag_list]
708 | end
709 | end
710 | end
711 | ```
712 |
713 | You can now retrieve these attributes from the policy:
714 |
715 | ```ruby
716 | # app/controllers/posts_controller.rb
717 | class PostsController < ApplicationController
718 | def update
719 | @post = Post.find(params[:id])
720 | if @post.update(post_params)
721 | redirect_to @post
722 | else
723 | render :edit
724 | end
725 | end
726 |
727 | private
728 |
729 | def post_params
730 | params.require(:post).permit(policy(@post).permitted_attributes)
731 | end
732 | end
733 | ```
734 |
735 | However, this is a bit cumbersome, so Pundit provides a convenient helper method:
736 |
737 | ```ruby
738 | # app/controllers/posts_controller.rb
739 | class PostsController < ApplicationController
740 | def update
741 | @post = Post.find(params[:id])
742 | if @post.update(permitted_attributes(@post))
743 | redirect_to @post
744 | else
745 | render :edit
746 | end
747 | end
748 | end
749 | ```
750 |
751 | If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:
752 |
753 | ```ruby
754 | # app/policies/post_policy.rb
755 | class PostPolicy < ApplicationPolicy
756 | def permitted_attributes_for_create
757 | [:title, :body]
758 | end
759 |
760 | def permitted_attributes_for_edit
761 | [:body]
762 | end
763 | end
764 | ```
765 |
766 | If you have defined an action-specific method on your policy for the current action, the `permitted_attributes` helper will call it instead of calling `permitted_attributes` on your controller.
767 |
768 | If you need to fetch parameters based on namespaces different from the suggested one, override the below method, in your controller, and return an instance of `ActionController::Parameters`.
769 |
770 | ```ruby
771 | def pundit_params_for(record)
772 | params.require(PolicyFinder.new(record).param_key)
773 | end
774 | ```
775 |
776 | For example:
777 |
778 | ```ruby
779 | # If you don't want to use require
780 | def pundit_params_for(record)
781 | params.fetch(PolicyFinder.new(record).param_key, {})
782 | end
783 |
784 | # If you are using something like the JSON API spec
785 | def pundit_params_for(_record)
786 | params.fetch(:data, {}).fetch(:attributes, {})
787 | end
788 | ```
789 |
790 | ## RSpec
791 |
792 | ### Policy Specs
793 |
794 | > [!TIP]
795 | > An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
796 | [excellent post](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/punditcommunity/pundit-matchers) gem.
797 |
798 | Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec.
799 | Require `pundit/rspec` in your `spec_helper.rb`:
800 |
801 | ``` ruby
802 | require "pundit/rspec"
803 | ```
804 |
805 | Then put your policy specs in `spec/policies`, and make them look somewhat like this:
806 |
807 | ``` ruby
808 | describe PostPolicy do
809 | subject { described_class }
810 |
811 | permissions :update?, :edit? do
812 | it "denies access if post is published" do
813 | expect(subject).not_to permit(User.new(admin: false), Post.new(published: true))
814 | end
815 |
816 | it "grants access if post is published and user is an admin" do
817 | expect(subject).to permit(User.new(admin: true), Post.new(published: true))
818 | end
819 |
820 | it "grants access if post is unpublished" do
821 | expect(subject).to permit(User.new(admin: false), Post.new(published: false))
822 | end
823 | end
824 | end
825 | ```
826 |
827 | ### Custom matcher description
828 |
829 | By default rspec includes an inspected `user` and `record` in the matcher description, which might become overly verbose:
830 |
831 | ```
832 | PostPolicy
833 | update? and show?
834 | is expected to permit # and #>
835 | ```
836 |
837 | You can override the default description with a static string, or a block:
838 |
839 | ```ruby
840 | # static alternative: Pundit::RSpec::Matchers.description = "permit the user"
841 | Pundit::RSpec::Matchers.description = ->(user, record) do
842 | "permit user with role #{user.role} to access record with ID #{record.id}"
843 | end
844 | ```
845 |
846 | Which would make for a less chatty output:
847 |
848 | ```
849 | PostPolicy
850 | update? and show?
851 | is expected to permit user with role admin to access record with ID 130
852 | ```
853 |
854 | ### Focus Support
855 |
856 | If your RSpec config has `filter_run_when_matching :focus`, you may tag the `permissions` helper like so:
857 |
858 | ```
859 | permissions :show?, :focus do
860 | ```
861 |
862 | ### Scope Specs
863 |
864 | Pundit does not provide a DSL for testing scopes. Test them like you would a regular Ruby class!
865 |
866 | ### Linting with RuboCop RSpec
867 |
868 | When you lint your RSpec spec files with `rubocop-rspec`, it will fail to properly detect RSpec constructs that Pundit defines, `permissions`.
869 | Make sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`:
870 |
871 | ```yaml
872 | inherit_gem:
873 | pundit: config/rubocop-rspec.yml
874 | ```
875 |
876 | # External Resources
877 |
878 | - [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
879 | - [Migrating to Pundit from CanCan](https://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
880 | - [Testing Pundit Policies with RSpec](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
881 | - [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450)
882 | - [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
883 | - [Straightforward Rails Authorization with Pundit](https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
884 |
885 | ## Other implementations
886 |
887 | - [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](https://flask.pocoo.org/) extension "heavily inspired by" Pundit
888 |
889 | # License
890 |
891 | Licensed under the MIT license, see the separate LICENSE.txt file.
892 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rubygems"
4 | require "bundler/gem_tasks"
5 | require "rspec/core/rake_task"
6 | require "yard"
7 | require "rubocop/rake_task"
8 |
9 | RuboCop::RakeTask.new
10 |
11 | desc "Run all examples"
12 | RSpec::Core::RakeTask.new(:spec) do |t|
13 | t.rspec_opts = %w[--color]
14 | end
15 |
16 | YARD::Rake::YardocTask.new do |t|
17 | t.files = ["lib/**/*.rb"]
18 | t.stats_options = ["--list-undoc"]
19 | end
20 |
21 | task default: :spec
22 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | Please do not file an issue on GitHub, or send a PR addressing the issue.
4 |
5 | ## Supported versions
6 |
7 | Most recent major version only.
8 |
9 | ## Reporting a vulnerability
10 |
11 | Contact one of the maintainers directly:
12 |
13 | * [@Burgestrand](https://github.com/Burgestrand)
14 | * [@dgmstuart](https://github.com/dgmstuart)
15 | * [@varvet](https://github.com/varvet)
16 |
17 | You can report vulnerabilities on GitHub too: https://github.com/varvet/pundit/security
18 |
19 | Thank you!
20 |
--------------------------------------------------------------------------------
/config/rubocop-rspec.yml:
--------------------------------------------------------------------------------
1 | RSpec:
2 | Language:
3 | ExampleGroups:
4 | Regular:
5 | - permissions
6 |
--------------------------------------------------------------------------------
/lib/generators/pundit/install/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Generates an application policy as a starting point for your application.
3 |
--------------------------------------------------------------------------------
/lib/generators/pundit/install/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # @private
5 | module Generators
6 | # @private
7 | class InstallGenerator < ::Rails::Generators::Base
8 | source_root File.expand_path("templates", __dir__)
9 |
10 | def copy_application_policy
11 | template "application_policy.rb.tt", "app/policies/application_policy.rb"
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/generators/pundit/install/templates/application_policy.rb.tt:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationPolicy
4 | attr_reader :user, :record
5 |
6 | def initialize(user, record)
7 | @user = user
8 | @record = record
9 | end
10 |
11 | def index?
12 | false
13 | end
14 |
15 | def show?
16 | false
17 | end
18 |
19 | def create?
20 | false
21 | end
22 |
23 | def new?
24 | create?
25 | end
26 |
27 | def update?
28 | false
29 | end
30 |
31 | def edit?
32 | update?
33 | end
34 |
35 | def destroy?
36 | false
37 | end
38 |
39 | class Scope
40 | def initialize(user, scope)
41 | @user = user
42 | @scope = scope
43 | end
44 |
45 | def resolve
46 | raise NoMethodError, "You must define #resolve in #{self.class}"
47 | end
48 |
49 | private
50 |
51 | attr_reader :user, :scope
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/generators/pundit/policy/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Generates a policy for a model with the given name.
3 |
4 | Example:
5 | rails generate pundit:policy user
6 |
7 | This will create:
8 | app/policies/user_policy.rb
9 |
--------------------------------------------------------------------------------
/lib/generators/pundit/policy/policy_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # @private
5 | module Generators
6 | # @private
7 | class PolicyGenerator < ::Rails::Generators::NamedBase
8 | source_root File.expand_path("templates", __dir__)
9 |
10 | def create_policy
11 | template "policy.rb.tt", File.join("app/policies", class_path, "#{file_name}_policy.rb")
12 | end
13 |
14 | hook_for :test_framework
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/generators/pundit/policy/templates/policy.rb.tt:
--------------------------------------------------------------------------------
1 | <% module_namespacing do -%>
2 | class <%= class_name %>Policy < ApplicationPolicy
3 | # NOTE: Up to Pundit v2.3.1, the inheritance was declared as
4 | # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`.
5 | # In most cases the behavior will be identical, but if updating existing
6 | # code, beware of possible changes to the ancestors:
7 | # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5
8 |
9 | class Scope < ApplicationPolicy::Scope
10 | # NOTE: Be explicit about which records you allow access to!
11 | # def resolve
12 | # scope.all
13 | # end
14 | end
15 | end
16 | <% end -%>
17 |
--------------------------------------------------------------------------------
/lib/generators/rspec/policy_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @private
4 | module Rspec
5 | # @private
6 | module Generators
7 | # @private
8 | class PolicyGenerator < ::Rails::Generators::NamedBase
9 | source_root File.expand_path("templates", __dir__)
10 |
11 | def create_policy_spec
12 | template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/generators/rspec/templates/policy_spec.rb.tt:
--------------------------------------------------------------------------------
1 | require '<%= File.exist?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
2 |
3 | RSpec.describe <%= class_name %>Policy, type: :policy do
4 | let(:user) { User.new }
5 |
6 | subject { described_class }
7 |
8 | permissions ".scope" do
9 | pending "add some examples to (or delete) #{__FILE__}"
10 | end
11 |
12 | permissions :show? do
13 | pending "add some examples to (or delete) #{__FILE__}"
14 | end
15 |
16 | permissions :create? do
17 | pending "add some examples to (or delete) #{__FILE__}"
18 | end
19 |
20 | permissions :update? do
21 | pending "add some examples to (or delete) #{__FILE__}"
22 | end
23 |
24 | permissions :destroy? do
25 | pending "add some examples to (or delete) #{__FILE__}"
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/policy_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @private
4 | module TestUnit
5 | # @private
6 | module Generators
7 | # @private
8 | class PolicyGenerator < ::Rails::Generators::NamedBase
9 | source_root File.expand_path("templates", __dir__)
10 |
11 | def create_policy_test
12 | template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/templates/policy_test.rb.tt:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class <%= class_name %>PolicyTest < ActiveSupport::TestCase
4 | def test_scope
5 | end
6 |
7 | def test_show
8 | end
9 |
10 | def test_create
11 | end
12 |
13 | def test_update
14 | end
15 |
16 | def test_destroy
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/pundit.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support"
4 |
5 | require "pundit/version"
6 | require "pundit/error"
7 | require "pundit/policy_finder"
8 | require "pundit/context"
9 | require "pundit/authorization"
10 | require "pundit/helper"
11 | require "pundit/cache_store"
12 | require "pundit/cache_store/null_store"
13 | require "pundit/cache_store/legacy_store"
14 | require "pundit/railtie" if defined?(Rails)
15 |
16 | # Hello? Yes, this is Pundit.
17 | #
18 | # @api public
19 | module Pundit
20 | # @api private
21 | # @since v1.0.0
22 | # @deprecated See {Pundit::PolicyFinder}
23 | SUFFIX = Pundit::PolicyFinder::SUFFIX
24 |
25 | # @api private
26 | # @private
27 | # @since v0.1.0
28 | module Generators; end
29 |
30 | def self.included(base)
31 | location = caller_locations(1, 1).first
32 | warn <<~WARNING
33 | 'include Pundit' is deprecated. Please use 'include Pundit::Authorization' instead.
34 | (called from #{location.label} at #{location.path}:#{location.lineno})
35 | WARNING
36 | base.include Authorization
37 | end
38 |
39 | class << self
40 | # @see Pundit::Context#authorize
41 | # @since v1.0.0
42 | def authorize(user, record, query, policy_class: nil, cache: nil)
43 | context = if cache
44 | policy_cache = CacheStore::LegacyStore.new(cache)
45 | Context.new(user: user, policy_cache: policy_cache)
46 | else
47 | Context.new(user: user)
48 | end
49 |
50 | context.authorize(record, query: query, policy_class: policy_class)
51 | end
52 |
53 | # @see Pundit::Context#policy_scope
54 | # @since v0.1.0
55 | def policy_scope(user, *args, **kwargs, &block)
56 | Context.new(user: user).policy_scope(*args, **kwargs, &block)
57 | end
58 |
59 | # @see Pundit::Context#policy_scope!
60 | # @since v0.1.0
61 | def policy_scope!(user, *args, **kwargs, &block)
62 | Context.new(user: user).policy_scope!(*args, **kwargs, &block)
63 | end
64 |
65 | # @see Pundit::Context#policy
66 | # @since v0.1.0
67 | def policy(user, *args, **kwargs, &block)
68 | Context.new(user: user).policy(*args, **kwargs, &block)
69 | end
70 |
71 | # @see Pundit::Context#policy!
72 | # @since v0.1.0
73 | def policy!(user, *args, **kwargs, &block)
74 | Context.new(user: user).policy!(*args, **kwargs, &block)
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/pundit/authorization.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # Pundit DSL to include in your controllers to provide authorization helpers.
5 | #
6 | # @example
7 | # class ApplicationController < ActionController::Base
8 | # include Pundit::Authorization
9 | # end
10 | # @see #pundit
11 | # @api public
12 | # @since v2.2.0
13 | module Authorization
14 | extend ActiveSupport::Concern
15 |
16 | included do
17 | helper Helper if respond_to?(:helper)
18 | if respond_to?(:helper_method)
19 | helper_method :policy
20 | helper_method :pundit_policy_scope
21 | helper_method :pundit_user
22 | end
23 | end
24 |
25 | protected
26 |
27 | # An instance of {Pundit::Context} initialized with the current user.
28 | #
29 | # @note this method is memoized and will return the same instance during the request.
30 | # @api public
31 | # @return [Pundit::Context]
32 | # @see #pundit_user
33 | # @see #policies
34 | # @since v2.3.2
35 | def pundit
36 | @pundit ||= Pundit::Context.new(
37 | user: pundit_user,
38 | policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
39 | )
40 | end
41 |
42 | # Hook method which allows customizing which user is passed to policies and
43 | # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
44 | #
45 | # @note Make sure to call `pundit_reset!` if this changes during a request.
46 | # @see https://github.com/varvet/pundit#customize-pundit-user
47 | # @see #pundit
48 | # @see #pundit_reset!
49 | # @return [Object] the user object to be used with pundit
50 | # @since v0.2.2
51 | def pundit_user
52 | current_user
53 | end
54 |
55 | # Clears the cached Pundit authorization data.
56 | #
57 | # This method should be called when the pundit_user is changed,
58 | # such as during user switching, to ensure that stale authorization
59 | # data is not used. Pundit caches authorization policies and scopes
60 | # for the pundit_user, so calling this method will reset those
61 | # caches and ensure that the next authorization checks are performed
62 | # with the correct context for the new pundit_user.
63 | #
64 | # @return [void]
65 | # @since v2.5.0
66 | def pundit_reset!
67 | @pundit = nil
68 | @_pundit_policies = nil
69 | @_pundit_policy_scopes = nil
70 | @_pundit_policy_authorized = nil
71 | @_pundit_policy_scoped = nil
72 | end
73 |
74 | # @!group Policies
75 |
76 | # Retrieves the policy for the given record, initializing it with the record
77 | # and current user and finally throwing an error if the user is not
78 | # authorized to perform the given action.
79 | #
80 | # @param record [Object, Array] the object we're checking permissions of
81 | # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
82 | # If omitted then this defaults to the Rails controller action name.
83 | # @param policy_class [Class] the policy class we want to force use of
84 | # @raise [NotAuthorizedError] if the given query method returned false
85 | # @return [record] Always returns the passed object record
86 | # @see Pundit::Context#authorize
87 | # @see #verify_authorized
88 | # @since v0.1.0
89 | def authorize(record, query = nil, policy_class: nil)
90 | query ||= "#{action_name}?"
91 |
92 | @_pundit_policy_authorized = true
93 |
94 | pundit.authorize(record, query: query, policy_class: policy_class)
95 | end
96 |
97 | # Allow this action not to perform authorization.
98 | #
99 | # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
100 | # @return [void]
101 | # @see #verify_authorized
102 | # @since v1.0.0
103 | def skip_authorization
104 | @_pundit_policy_authorized = :skipped
105 | end
106 |
107 | # @return [Boolean] wether or not authorization has been performed
108 | # @see #authorize
109 | # @see #skip_authorization
110 | # @since v1.0.0
111 | def pundit_policy_authorized?
112 | !!@_pundit_policy_authorized
113 | end
114 |
115 | # Raises an error if authorization has not been performed.
116 | #
117 | # Usually used as an `after_action` filter to prevent programmer error in
118 | # forgetting to call {#authorize} or {#skip_authorization}.
119 | #
120 | # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
121 | # @raise [AuthorizationNotPerformedError] if authorization has not been performed
122 | # @return [void]
123 | # @see #authorize
124 | # @see #skip_authorization
125 | # @since v0.1.0
126 | def verify_authorized
127 | raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
128 | end
129 |
130 | # rubocop:disable Naming/MemoizedInstanceVariableName
131 |
132 | # Cache of policies. You should not rely on this method.
133 | #
134 | # @api private
135 | # @since v1.0.0
136 | def policies
137 | @_pundit_policies ||= {}
138 | end
139 |
140 | # rubocop:enable Naming/MemoizedInstanceVariableName
141 |
142 | # @!endgroup
143 |
144 | # Retrieves the policy for the given record.
145 | #
146 | # @see https://github.com/varvet/pundit#policies
147 | # @param record [Object] the object we're retrieving the policy for
148 | # @return [Object] instance of policy class with query methods
149 | # @since v0.1.0
150 | def policy(record)
151 | pundit.policy!(record)
152 | end
153 |
154 | # @!group Policy Scopes
155 |
156 | # Retrieves the policy scope for the given record.
157 | #
158 | # @see https://github.com/varvet/pundit#scopes
159 | # @param scope [Object] the object we're retrieving the policy scope for
160 | # @param policy_scope_class [#resolve] the policy scope class we want to force use of
161 | # @return [#resolve, nil] instance of scope class which can resolve to a scope
162 | # @since v0.1.0
163 | def policy_scope(scope, policy_scope_class: nil)
164 | @_pundit_policy_scoped = true
165 | policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
166 | end
167 |
168 | # Allow this action not to perform policy scoping.
169 | #
170 | # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
171 | # @return [void]
172 | # @see #verify_policy_scoped
173 | # @since v1.0.0
174 | def skip_policy_scope
175 | @_pundit_policy_scoped = :skipped
176 | end
177 |
178 | # @return [Boolean] wether or not policy scoping has been performed
179 | # @see #policy_scope
180 | # @see #skip_policy_scope
181 | # @since v1.0.0
182 | def pundit_policy_scoped?
183 | !!@_pundit_policy_scoped
184 | end
185 |
186 | # Raises an error if policy scoping has not been performed.
187 | #
188 | # Usually used as an `after_action` filter to prevent programmer error in
189 | # forgetting to call {#policy_scope} or {#skip_policy_scope} in index
190 | # actions.
191 | #
192 | # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
193 | # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
194 | # @return [void]
195 | # @see #policy_scope
196 | # @see #skip_policy_scope
197 | # @since v0.2.1
198 | def verify_policy_scoped
199 | raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
200 | end
201 |
202 | # rubocop:disable Naming/MemoizedInstanceVariableName
203 |
204 | # Cache of policy scope. You should not rely on this method.
205 | #
206 | # @api private
207 | # @since v1.0.0
208 | def policy_scopes
209 | @_pundit_policy_scopes ||= {}
210 | end
211 |
212 | # rubocop:enable Naming/MemoizedInstanceVariableName
213 |
214 | # This was added to allow calling `policy_scope!` without flipping the
215 | # `pundit_policy_scoped?` flag.
216 | #
217 | # It's used internally by `policy_scope`, as well as from the views
218 | # when they call `policy_scope`. It works because views get their helper
219 | # from {Pundit::Helper}.
220 | #
221 | # @note This also memoizes the instance with `scope` as the key.
222 | # @see Pundit::Helper#policy_scope
223 | # @api private
224 | # @since v1.0.0
225 | def pundit_policy_scope(scope)
226 | policy_scopes[scope] ||= pundit.policy_scope!(scope)
227 | end
228 | private :pundit_policy_scope
229 |
230 | # @!endgroup
231 |
232 | # @!group Strong Parameters
233 |
234 | # Retrieves a set of permitted attributes from the policy.
235 | #
236 | # Done by instantiating the policy class for the given record and calling
237 | # `permitted_attributes` on it, or `permitted_attributes_for_{action}` if
238 | # `action` is defined. It then infers what key the record should have in the
239 | # params hash and retrieves the permitted attributes from the params hash
240 | # under that key.
241 | #
242 | # @see https://github.com/varvet/pundit#strong-parameters
243 | # @param record [Object] the object we're retrieving permitted attributes for
244 | # @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
245 | # If omitted then this defaults to the Rails controller action name.
246 | # @return [Hash{String => Object}] the permitted attributes
247 | # @since v1.0.0
248 | def permitted_attributes(record, action = action_name)
249 | policy = policy(record)
250 | method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
251 | "permitted_attributes_for_#{action}"
252 | else
253 | "permitted_attributes"
254 | end
255 | pundit_params_for(record).permit(*policy.public_send(method_name))
256 | end
257 |
258 | # Retrieves the params for the given record.
259 | #
260 | # @param record [Object] the object we're retrieving params for
261 | # @return [ActionController::Parameters] the params
262 | # @since v2.0.0
263 | def pundit_params_for(record)
264 | params.require(PolicyFinder.new(record).param_key)
265 | end
266 |
267 | # @!endgroup
268 | end
269 | end
270 |
--------------------------------------------------------------------------------
/lib/pundit/cache_store.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # Namespace for cache store implementations.
5 | #
6 | # Cache stores are used to cache policy lookups, so you get the same policy
7 | # instance for the same record.
8 | # @since v2.3.2
9 | module CacheStore
10 | # @!group Cache Store Interface
11 |
12 | # @!method fetch(user:, record:, &block)
13 | # Looks up a stored policy or generate a new one.
14 | #
15 | # @since v2.3.2
16 | # @note This is a method template, but the method does not exist in this module.
17 | # @param user [Object] the user that initiated the action
18 | # @param record [Object] the object being accessed
19 | # @param block [Proc] the block to execute if missing
20 | # @return [Object] the policy
21 |
22 | # @!endgroup
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/pundit/cache_store/legacy_store.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | module CacheStore
5 | # A cache store that uses only the record as a cache key, and ignores the user.
6 | #
7 | # The original cache mechanism used by Pundit.
8 | #
9 | # @api private
10 | # @since v2.3.2
11 | class LegacyStore
12 | # @since v2.3.2
13 | def initialize(hash = {})
14 | @store = hash
15 | end
16 |
17 | # A cache store that uses only the record as a cache key, and ignores the user.
18 | #
19 | # @note `nil` results are not cached.
20 | # @since v2.3.2
21 | def fetch(user:, record:)
22 | _ = user
23 | @store[record] ||= yield
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/pundit/cache_store/null_store.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | module CacheStore
5 | # A cache store that does not cache anything.
6 | #
7 | # Use `NullStore.instance` to get the singleton instance, it is thread-safe.
8 | #
9 | # @see Pundit::Context#initialize
10 | # @api private
11 | # @since v2.3.2
12 | class NullStore
13 | @instance = new
14 |
15 | class << self
16 | # @since v2.3.2
17 | # @return [NullStore] the singleton instance
18 | attr_reader :instance
19 | end
20 |
21 | # Always yields, does not cache anything.
22 | # @yield
23 | # @return [any] whatever the block returns.
24 | # @since v2.3.2
25 | def fetch(*, **)
26 | yield
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/pundit/context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # {Pundit::Context} is intended to be created once per request and user, and
5 | # it is then used to perform authorization checks throughout the request.
6 | #
7 | # @example Using Sinatra
8 | # helpers do
9 | # def current_user = ...
10 | #
11 | # def pundit
12 | # @pundit ||= Pundit::Context.new(user: current_user)
13 | # end
14 | # end
15 | #
16 | # get "/posts/:id" do |id|
17 | # pundit.authorize(Post.find(id), query: :show?)
18 | # end
19 | #
20 | # @example Using [Roda](https://roda.jeremyevans.net/index.html)
21 | # route do |r|
22 | # context = Pundit::Context.new(user:)
23 | #
24 | # r.get "posts", Integer do |id|
25 | # context.authorize(Post.find(id), query: :show?)
26 | # end
27 | # end
28 | #
29 | # @since v2.3.2
30 | class Context
31 | # @see Pundit::Authorization#pundit
32 | # @param user later passed to policies and scopes
33 | # @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore})
34 | # @since v2.3.2
35 | def initialize(user:, policy_cache: CacheStore::NullStore.instance)
36 | @user = user
37 | @policy_cache = policy_cache
38 | end
39 |
40 | # @api public
41 | # @see #initialize
42 | # @since v2.3.2
43 | attr_reader :user
44 |
45 | # @api private
46 | # @see #initialize
47 | # @since v2.3.2
48 | attr_reader :policy_cache
49 |
50 | # @!group Policies
51 |
52 | # Retrieves the policy for the given record, initializing it with the
53 | # record and user and finally throwing an error if the user is not
54 | # authorized to perform the given action.
55 | #
56 | # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
57 | # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
58 | # @param policy_class [Class] the policy class we want to force use of
59 | # @raise [NotAuthorizedError] if the given query method returned false
60 | # @return [Object] Always returns the passed object record
61 | # @since v2.3.2
62 | def authorize(possibly_namespaced_record, query:, policy_class:)
63 | record = pundit_model(possibly_namespaced_record)
64 | policy = if policy_class
65 | policy_class.new(user, record)
66 | else
67 | policy!(possibly_namespaced_record)
68 | end
69 |
70 | raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
71 |
72 | record
73 | end
74 |
75 | # Retrieves the policy for the given record.
76 | #
77 | # @see https://github.com/varvet/pundit#policies
78 | # @param record [Object] the object we're retrieving the policy for
79 | # @raise [InvalidConstructorError] if the policy constructor called incorrectly
80 | # @return [Object, nil] instance of policy class with query methods
81 | # @since v2.3.2
82 | def policy(record)
83 | cached_find(record, &:policy)
84 | end
85 |
86 | # Retrieves the policy for the given record, or raises if not found.
87 | #
88 | # @see https://github.com/varvet/pundit#policies
89 | # @param record [Object] the object we're retrieving the policy for
90 | # @raise [NotDefinedError] if the policy cannot be found
91 | # @raise [InvalidConstructorError] if the policy constructor called incorrectly
92 | # @return [Object] instance of policy class with query methods
93 | # @since v2.3.2
94 | def policy!(record)
95 | cached_find(record, &:policy!)
96 | end
97 |
98 | # @!endgroup
99 |
100 | # @!group Scopes
101 |
102 | # Retrieves the policy scope for the given record.
103 | #
104 | # @see https://github.com/varvet/pundit#scopes
105 | # @param scope [Object] the object we're retrieving the policy scope for
106 | # @raise [InvalidConstructorError] if the policy constructor called incorrectly
107 | # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
108 | # @since v2.3.2
109 | def policy_scope(scope)
110 | policy_scope_class = policy_finder(scope).scope
111 | return unless policy_scope_class
112 |
113 | begin
114 | policy_scope = policy_scope_class.new(user, pundit_model(scope))
115 | rescue ArgumentError
116 | raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
117 | end
118 |
119 | policy_scope.resolve
120 | end
121 |
122 | # Retrieves the policy scope for the given record. Raises if not found.
123 | #
124 | # @see https://github.com/varvet/pundit#scopes
125 | # @param scope [Object] the object we're retrieving the policy scope for
126 | # @raise [NotDefinedError] if the policy scope cannot be found
127 | # @raise [InvalidConstructorError] if the policy constructor called incorrectly
128 | # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
129 | # @since v2.3.2
130 | def policy_scope!(scope)
131 | policy_scope_class = policy_finder(scope).scope!
132 |
133 | begin
134 | policy_scope = policy_scope_class.new(user, pundit_model(scope))
135 | rescue ArgumentError
136 | raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
137 | end
138 |
139 | policy_scope.resolve
140 | end
141 |
142 | # @!endgroup
143 |
144 | private
145 |
146 | # @!group Private Helpers
147 |
148 | # Finds a cached policy for the given record, or yields to find one.
149 | #
150 | # @api private
151 | # @param record [Object] the object we're retrieving the policy for
152 | # @yield a policy finder if no policy was cached
153 | # @yieldparam [PolicyFinder] policy_finder
154 | # @yieldreturn [#new(user, model)]
155 | # @return [Policy, nil] an instantiated policy
156 | # @raise [InvalidConstructorError] if policy can't be instantated
157 | # @since v2.3.2
158 | def cached_find(record)
159 | policy_cache.fetch(user: user, record: record) do
160 | klass = yield policy_finder(record)
161 | next unless klass
162 |
163 | model = pundit_model(record)
164 |
165 | begin
166 | klass.new(user, model)
167 | rescue ArgumentError
168 | raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
169 | end
170 | end
171 | end
172 |
173 | # Return a policy finder for the given record.
174 | #
175 | # @api private
176 | # @return [PolicyFinder]
177 | # @since v2.3.2
178 | def policy_finder(record)
179 | PolicyFinder.new(record)
180 | end
181 |
182 | # Given a possibly namespaced record, return the actual record.
183 | #
184 | # @api private
185 | # @since v2.3.2
186 | def pundit_model(record)
187 | record.is_a?(Array) ? record.last : record
188 | end
189 | end
190 | end
191 |
--------------------------------------------------------------------------------
/lib/pundit/error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # @api private
5 | # @since v1.0.0
6 | # To avoid name clashes with common Error naming when mixing in Pundit,
7 | # keep it here with compact class style definition.
8 | class Error < StandardError; end
9 |
10 | # Error that will be raised when authorization has failed
11 | # @since v0.1.0
12 | class NotAuthorizedError < Error
13 | # @see #initialize
14 | # @since v0.2.3
15 | attr_reader :query
16 | # @see #initialize
17 | # @since v0.2.3
18 | attr_reader :record
19 | # @see #initialize
20 | # @since v0.2.3
21 | attr_reader :policy
22 |
23 | # @since v1.0.0
24 | #
25 | # @overload initialize(message)
26 | # Create an error with a simple error message.
27 | # @param [String] message A simple error message string.
28 | #
29 | # @overload initialize(options)
30 | # Create an error with the specified attributes.
31 | # @param [Hash] options The error options.
32 | # @option options [String] :message Optional custom error message. Will default to a generalized message.
33 | # @option options [Symbol] :query The name of the policy method that was checked.
34 | # @option options [Object] :record The object that was being checked with the policy.
35 | # @option options [Class] :policy The class of policy that was used for the check.
36 | def initialize(options = {})
37 | if options.is_a? String
38 | message = options
39 | else
40 | @query = options[:query]
41 | @record = options[:record]
42 | @policy = options[:policy]
43 |
44 | message = options.fetch(:message) do
45 | record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}"
46 | "not allowed to #{policy.class}##{query} #{record_name}"
47 | end
48 | end
49 |
50 | super(message)
51 | end
52 | end
53 |
54 | # Error that will be raised if a policy or policy scope constructor is not called correctly.
55 | # @since v2.0.0
56 | class InvalidConstructorError < Error; end
57 |
58 | # Error that will be raised if a controller action has not called the
59 | # `authorize` or `skip_authorization` methods.
60 | # @since v0.2.3
61 | class AuthorizationNotPerformedError < Error; end
62 |
63 | # Error that will be raised if a controller action has not called the
64 | # `policy_scope` or `skip_policy_scope` methods.
65 | # @since v0.3.0
66 | class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
67 |
68 | # Error that will be raised if a policy or policy scope is not defined.
69 | # @since v0.1.0
70 | class NotDefinedError < Error; end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/pundit/helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # Rails view helpers, to allow a slightly different view-specific
5 | # implementation of the methods in {Pundit::Authorization}.
6 | #
7 | # @api private
8 | # @since v1.0.0
9 | module Helper
10 | # @see Pundit::Authorization#pundit_policy_scope
11 | # @since v1.0.0
12 | def policy_scope(scope)
13 | pundit_policy_scope(scope)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/pundit/policy_finder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # String#safe_constantize, String#demodulize, String#underscore, String#camelize
4 | require "active_support/core_ext/string/inflections"
5 |
6 | module Pundit
7 | # Finds policy and scope classes for given object.
8 | # @since v0.1.0
9 | # @api public
10 | # @example
11 | # user = User.find(params[:id])
12 | # finder = PolicyFinder.new(user)
13 | # finder.policy #=> UserPolicy
14 | # finder.scope #=> UserPolicy::Scope
15 | #
16 | class PolicyFinder
17 | # A constant applied to the end of the class name to find the policy class.
18 | #
19 | # @api private
20 | # @since v2.5.0
21 | SUFFIX = "Policy"
22 |
23 | # @see #initialize
24 | # @since v0.1.0
25 | attr_reader :object
26 |
27 | # @param object [any] the object to find policy and scope classes for
28 | # @since v0.1.0
29 | def initialize(object)
30 | @object = object
31 | end
32 |
33 | # @return [nil, Scope{#resolve}] scope class which can resolve to a scope
34 | # @see https://github.com/varvet/pundit#scopes
35 | # @example
36 | # scope = finder.scope #=> UserPolicy::Scope
37 | # scope.resolve #=> <#ActiveRecord::Relation ...>
38 | #
39 | # @since v0.1.0
40 | def scope
41 | "#{policy}::Scope".safe_constantize
42 | end
43 |
44 | # @return [nil, Class] policy class with query methods
45 | # @see https://github.com/varvet/pundit#policies
46 | # @example
47 | # policy = finder.policy #=> UserPolicy
48 | # policy.show? #=> true
49 | # policy.update? #=> false
50 | #
51 | # @since v0.1.0
52 | def policy
53 | klass = find(object)
54 | klass.is_a?(String) ? klass.safe_constantize : klass
55 | end
56 |
57 | # @return [Scope{#resolve}] scope class which can resolve to a scope
58 | # @raise [NotDefinedError] if scope could not be determined
59 | #
60 | # @since v0.1.0
61 | def scope!
62 | scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
63 | end
64 |
65 | # @return [Class] policy class with query methods
66 | # @raise [NotDefinedError] if policy could not be determined
67 | #
68 | # @since v0.1.0
69 | def policy!
70 | policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
71 | end
72 |
73 | # @return [String] the name of the key this object would have in a params hash
74 | #
75 | # @since v1.1.0
76 | def param_key # rubocop:disable Metrics/AbcSize
77 | model = object.is_a?(Array) ? object.last : object
78 |
79 | if model.respond_to?(:model_name)
80 | model.model_name.param_key.to_s
81 | elsif model.is_a?(Class)
82 | model.to_s.demodulize.underscore
83 | else
84 | model.class.to_s.demodulize.underscore
85 | end
86 | end
87 |
88 | private
89 |
90 | # Given an object, find the policy class name.
91 | #
92 | # Uses recursion to handle namespaces.
93 | #
94 | # @return [String, Class] the policy class, or its name.
95 | # @since v0.2.0
96 | def find(subject)
97 | if subject.is_a?(Array)
98 | modules = subject.dup
99 | last = modules.pop
100 | context = modules.map { |x| find_class_name(x) }.join("::")
101 | [context, find(last)].join("::")
102 | elsif subject.respond_to?(:policy_class)
103 | subject.policy_class
104 | elsif subject.class.respond_to?(:policy_class)
105 | subject.class.policy_class
106 | else
107 | klass = find_class_name(subject)
108 | "#{klass}#{SUFFIX}"
109 | end
110 | end
111 |
112 | # Given an object, find its' class name.
113 | #
114 | # - Supports ActiveModel.
115 | # - Supports regular classes.
116 | # - Supports symbols.
117 | # - Supports object instances.
118 | #
119 | # @return [String, Class] the class, or its name.
120 | # @since v1.1.0
121 | def find_class_name(subject)
122 | if subject.respond_to?(:model_name)
123 | subject.model_name
124 | elsif subject.class.respond_to?(:model_name)
125 | subject.class.model_name
126 | elsif subject.is_a?(Class)
127 | subject
128 | elsif subject.is_a?(Symbol)
129 | subject.to_s.camelize
130 | else
131 | subject.class
132 | end
133 | end
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/lib/pundit/railtie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # @since v2.5.0
5 | class Railtie < Rails::Railtie
6 | if Rails.version.to_f >= 8.0
7 | initializer "pundit.stats_directories" do
8 | require "rails/code_statistics"
9 |
10 | if Rails.root.join("app/policies").directory?
11 | Rails::CodeStatistics.register_directory("Policies", "app/policies")
12 | end
13 |
14 | if Rails.root.join("test/policies").directory?
15 | Rails::CodeStatistics.register_directory("Policy tests", "test/policies", test_directory: true)
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/pundit/rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "pundit"
4 | # Array#to_sentence
5 | require "active_support/core_ext/array/conversions"
6 |
7 | module Pundit
8 | # Namespace for Pundit's RSpec integration.
9 | # @since v0.1.0
10 | module RSpec
11 | # Namespace for Pundit's RSpec matchers.
12 | module Matchers
13 | extend ::RSpec::Matchers::DSL
14 |
15 | # @!method description=(description)
16 | class << self
17 | # Used to build a suitable description for the Pundit `permit` matcher.
18 | # @api public
19 | # @param value [String, Proc]
20 | # @example
21 | # Pundit::RSpec::Matchers.description = ->(user, record) do
22 | # "permit user with role #{user.role} to access record with ID #{record.id}"
23 | # end
24 | attr_writer :description
25 |
26 | # Used to retrieve a suitable description for the Pundit `permit` matcher.
27 | # @api private
28 | # @private
29 | def description(user, record)
30 | return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
31 |
32 | @description
33 | end
34 | end
35 |
36 | # rubocop:disable Metrics/BlockLength
37 | matcher :permit do |user, record|
38 | match_proc = lambda do |policy|
39 | @violating_permissions = permissions.find_all do |permission|
40 | !policy.new(user, record).public_send(permission)
41 | end
42 | @violating_permissions.empty?
43 | end
44 |
45 | match_when_negated_proc = lambda do |policy|
46 | @violating_permissions = permissions.find_all do |permission|
47 | policy.new(user, record).public_send(permission)
48 | end
49 | @violating_permissions.empty?
50 | end
51 |
52 | failure_message_proc = lambda do |policy|
53 | "Expected #{policy} to grant #{permissions.to_sentence} on " \
54 | "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted"
55 | end
56 |
57 | failure_message_when_negated_proc = lambda do |policy|
58 | "Expected #{policy} not to grant #{permissions.to_sentence} on " \
59 | "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted"
60 | end
61 |
62 | def was_or_were
63 | if @violating_permissions.count > 1
64 | "were"
65 | else
66 | "was"
67 | end
68 | end
69 |
70 | description do
71 | Pundit::RSpec::Matchers.description(user, record) || super()
72 | end
73 |
74 | if respond_to?(:match_when_negated)
75 | match(&match_proc)
76 | match_when_negated(&match_when_negated_proc)
77 | failure_message(&failure_message_proc)
78 | failure_message_when_negated(&failure_message_when_negated_proc)
79 | else
80 | # :nocov:
81 | # Compatibility with RSpec < 3.0, released 2014-06-01.
82 | match_for_should(&match_proc)
83 | match_for_should_not(&match_when_negated_proc)
84 | failure_message_for_should(&failure_message_proc)
85 | failure_message_for_should_not(&failure_message_when_negated_proc)
86 | # :nocov:
87 | end
88 |
89 | if ::RSpec.respond_to?(:current_example)
90 | def current_example
91 | ::RSpec.current_example
92 | end
93 | else
94 | # :nocov:
95 | # Compatibility with RSpec < 3.0, released 2014-06-01.
96 | def current_example
97 | example
98 | end
99 | # :nocov:
100 | end
101 |
102 | def permissions
103 | current_example.metadata.fetch(:permissions) do
104 | raise KeyError, <<~ERROR.strip
105 | No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
106 | ERROR
107 | end
108 | end
109 | end
110 | # rubocop:enable Metrics/BlockLength
111 | end
112 |
113 | # Mixed in to all policy example groups to provide a DSL.
114 | module DSL
115 | # @example
116 | # describe PostPolicy do
117 | # permissions :show?, :update? do
118 | # it { is_expected.to permit(user, own_post) }
119 | # end
120 | # end
121 | #
122 | # @example focused example group
123 | # describe PostPolicy do
124 | # permissions :show?, :update?, :focus do
125 | # it { is_expected.to permit(user, own_post) }
126 | # end
127 | # end
128 | #
129 | # @param list [Symbol, Array] a permission to describe
130 | # @return [void]
131 | def permissions(*list, &block)
132 | metadata = { permissions: list, caller: caller }
133 |
134 | if list.last == :focus
135 | list.pop
136 | metadata[:focus] = true
137 | end
138 |
139 | description = list.to_sentence
140 | describe(description, metadata) { instance_eval(&block) }
141 | end
142 | end
143 |
144 | # Mixed in to all policy example groups.
145 | #
146 | # @private not useful
147 | module PolicyExampleGroup
148 | include Pundit::RSpec::Matchers
149 |
150 | def self.included(base)
151 | base.metadata[:type] = :policy
152 | base.extend Pundit::RSpec::DSL
153 | super
154 | end
155 | end
156 | end
157 | end
158 |
159 | RSpec.configure do |config|
160 | config.include(
161 | Pundit::RSpec::PolicyExampleGroup,
162 | type: :policy,
163 | file_path: %r{spec/policies}
164 | )
165 | end
166 |
--------------------------------------------------------------------------------
/lib/pundit/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Pundit
4 | # The current version of Pundit.
5 | VERSION = "2.5.0"
6 | end
7 |
--------------------------------------------------------------------------------
/pundit.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path("lib", __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require "pundit/version"
6 |
7 | Gem::Specification.new do |gem|
8 | gem.name = "pundit"
9 | gem.version = Pundit::VERSION
10 | gem.authors = ["Jonas Nicklas", "Varvet AB"]
11 | gem.email = ["jonas.nicklas@gmail.com", "info@varvet.com"]
12 | gem.description = "Object oriented authorization for Rails applications"
13 | gem.summary = "OO authorization for Rails"
14 | gem.homepage = "https://github.com/varvet/pundit"
15 | gem.license = "MIT"
16 |
17 | Dir.chdir(__dir__) do
18 | gem.files = `git ls-files -z`.split("\x0").select do |f|
19 | f.start_with?("lib/", "README", "SECURITY", "LICENSE", "CHANGELOG")
20 | end
21 | end
22 | gem.require_paths = ["lib"]
23 |
24 | gem.metadata = {
25 | "rubygems_mfa_required" => "true",
26 | "bug_tracker_uri" => "https://github.com/varvet/pundit/issues",
27 | "changelog_uri" => "https://github.com/varvet/pundit/blob/main/CHANGELOG.md",
28 | "documentation_uri" => "https://github.com/varvet/pundit/blob/main/README.md",
29 | "homepage_uri" => "https://github.com/varvet/pundit",
30 | "source_code_uri" => "https://github.com/varvet/pundit"
31 | }
32 |
33 | gem.add_dependency "activesupport", ">= 3.0.0"
34 | end
35 |
--------------------------------------------------------------------------------
/spec/authorization_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "action_controller/metal/strong_parameters"
5 |
6 | describe Pundit::Authorization do
7 | def to_params(*args, **kwargs, &block)
8 | ActionController::Parameters.new(*args, **kwargs, &block)
9 | end
10 |
11 | let(:controller) { Controller.new(user, "update", to_params({})) }
12 | let(:user) { double("user") }
13 | let(:post) { Post.new(user) }
14 | let(:comment) { Comment.new }
15 | let(:article) { Article.new }
16 | let(:article_tag) { ArticleTag.new }
17 | let(:wiki) { Wiki.new }
18 |
19 | describe "#verify_authorized" do
20 | it "does nothing when authorized" do
21 | controller.authorize(post)
22 | controller.verify_authorized
23 | end
24 |
25 | it "raises an exception when not authorized" do
26 | expect { controller.verify_authorized }.to raise_error(Pundit::AuthorizationNotPerformedError)
27 | end
28 | end
29 |
30 | describe "#verify_policy_scoped" do
31 | it "does nothing when policy_scope is used" do
32 | controller.policy_scope(Post)
33 | controller.verify_policy_scoped
34 | end
35 |
36 | it "raises an exception when policy_scope is not used" do
37 | expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError)
38 | end
39 | end
40 |
41 | describe "#pundit_policy_authorized?" do
42 | it "is true when authorized" do
43 | controller.authorize(post)
44 | expect(controller.pundit_policy_authorized?).to be true
45 | end
46 |
47 | it "is false when not authorized" do
48 | expect(controller.pundit_policy_authorized?).to be false
49 | end
50 | end
51 |
52 | describe "#pundit_policy_scoped?" do
53 | it "is true when policy_scope is used" do
54 | controller.policy_scope(Post)
55 | expect(controller.pundit_policy_scoped?).to be true
56 | end
57 |
58 | it "is false when policy scope is not used" do
59 | expect(controller.pundit_policy_scoped?).to be false
60 | end
61 | end
62 |
63 | describe "#authorize" do
64 | it "infers the policy name and authorizes based on it" do
65 | expect(controller.authorize(post)).to be_truthy
66 | end
67 |
68 | it "returns the record on successful authorization" do
69 | expect(controller.authorize(post)).to eq(post)
70 | end
71 |
72 | it "returns the record when passed record with namespace " do
73 | expect(controller.authorize([:project, comment], :update?)).to eq(comment)
74 | end
75 |
76 | it "returns the record when passed record with nested namespace " do
77 | expect(controller.authorize([:project, :admin, comment], :update?)).to eq(comment)
78 | end
79 |
80 | it "returns the policy name symbol when passed record with headless policy" do
81 | expect(controller.authorize(:publication, :create?)).to eq(:publication)
82 | end
83 |
84 | it "returns the class when passed record not a particular instance" do
85 | expect(controller.authorize(Post, :show?)).to eq(Post)
86 | end
87 |
88 | it "can be given a different permission to check" do
89 | expect(controller.authorize(post, :show?)).to be_truthy
90 | expect { controller.authorize(post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
91 | end
92 |
93 | it "can be given a different policy class" do
94 | expect(controller.authorize(post, :create?, policy_class: PublicationPolicy)).to be_truthy
95 | end
96 |
97 | it "works with anonymous class policies" do
98 | expect(controller.authorize(article_tag, :show?)).to be_truthy
99 | expect { controller.authorize(article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
100 | end
101 |
102 | it "throws an exception when the permission check fails" do
103 | expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError)
104 | end
105 |
106 | it "throws an exception when a policy cannot be found" do
107 | expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError)
108 | end
109 |
110 | it "caches the policy" do
111 | expect(controller.policies[post]).to be_nil
112 | controller.authorize(post)
113 | expect(controller.policies[post]).not_to be_nil
114 | end
115 |
116 | it "raises an error when the given record is nil" do
117 | expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
118 | end
119 |
120 | it "raises an error with a invalid policy constructor" do
121 | expect { controller.authorize(wiki, :destroy?) }.to raise_error(Pundit::InvalidConstructorError)
122 | end
123 | end
124 |
125 | describe "#skip_authorization" do
126 | it "disables authorization verification" do
127 | controller.skip_authorization
128 | expect { controller.verify_authorized }.not_to raise_error
129 | end
130 | end
131 |
132 | describe "#skip_policy_scope" do
133 | it "disables policy scope verification" do
134 | controller.skip_policy_scope
135 | expect { controller.verify_policy_scoped }.not_to raise_error
136 | end
137 | end
138 |
139 | describe "#pundit_user" do
140 | it "returns the same thing as current_user" do
141 | expect(controller.pundit_user).to eq controller.current_user
142 | end
143 | end
144 |
145 | describe "#policy" do
146 | it "returns an instantiated policy" do
147 | policy = controller.policy(post)
148 | expect(policy.user).to eq user
149 | expect(policy.post).to eq post
150 | end
151 |
152 | it "throws an exception if the given policy can't be found" do
153 | expect { controller.policy(article) }.to raise_error(Pundit::NotDefinedError)
154 | end
155 |
156 | it "raises an error with a invalid policy constructor" do
157 | expect { controller.policy(wiki) }.to raise_error(Pundit::InvalidConstructorError)
158 | end
159 |
160 | it "allows policy to be injected" do
161 | new_policy = double
162 | controller.policies[post] = new_policy
163 |
164 | expect(controller.policy(post)).to eq new_policy
165 | end
166 | end
167 |
168 | describe "#policy_scope" do
169 | it "returns an instantiated policy scope" do
170 | expect(controller.policy_scope(Post)).to eq :published
171 | end
172 |
173 | it "allows policy scope class to be overridden" do
174 | expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
175 | end
176 |
177 | it "throws an exception if the given policy can't be found" do
178 | expect { controller.policy_scope(Article) }.to raise_error(Pundit::NotDefinedError)
179 | end
180 |
181 | it "raises an error with a invalid policy scope constructor" do
182 | expect { controller.policy_scope(Wiki) }.to raise_error(Pundit::InvalidConstructorError)
183 | end
184 |
185 | it "allows policy_scope to be injected" do
186 | new_scope = double
187 | controller.policy_scopes[Post] = new_scope
188 |
189 | expect(controller.policy_scope(Post)).to eq new_scope
190 | end
191 | end
192 |
193 | describe "#permitted_attributes" do
194 | it "checks policy for permitted attributes" do
195 | params = to_params(
196 | post: {
197 | title: "Hello",
198 | votes: 5,
199 | admin: true
200 | }
201 | )
202 |
203 | action = "update"
204 |
205 | expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq(
206 | "title" => "Hello",
207 | "votes" => 5
208 | )
209 | expect(Controller.new(double, action, params).permitted_attributes(post).to_h).to eq("votes" => 5)
210 | end
211 |
212 | it "checks policy for permitted attributes for record of a ActiveModel type" do
213 | customer_post = Customer::Post.new(user)
214 | params = to_params(
215 | customer_post: {
216 | title: "Hello",
217 | votes: 5,
218 | admin: true
219 | }
220 | )
221 |
222 | action = "update"
223 |
224 | expect(Controller.new(user, action, params).permitted_attributes(customer_post).to_h).to eq(
225 | "title" => "Hello",
226 | "votes" => 5
227 | )
228 | expect(Controller.new(double, action, params).permitted_attributes(customer_post).to_h).to eq(
229 | "votes" => 5
230 | )
231 | end
232 |
233 | it "goes through the policy cache" do
234 | params = to_params(post: { title: "Hello" })
235 | user = double
236 | post = Post.new(user)
237 | controller = Controller.new(user, "update", params)
238 |
239 | expect do
240 | expect(controller.permitted_attributes(post)).to be_truthy
241 | expect(controller.permitted_attributes(post)).to be_truthy
242 | end.to change { PostPolicy.instances }.by(1)
243 | end
244 | end
245 |
246 | describe "#permitted_attributes_for_action" do
247 | it "is checked if it is defined in the policy" do
248 | params = to_params(
249 | post: {
250 | title: "Hello",
251 | body: "blah",
252 | votes: 5,
253 | admin: true
254 | }
255 | )
256 |
257 | action = "revise"
258 | expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah")
259 | end
260 |
261 | it "can be explicitly set" do
262 | params = to_params(
263 | post: {
264 | title: "Hello",
265 | body: "blah",
266 | votes: 5,
267 | admin: true
268 | }
269 | )
270 |
271 | action = "update"
272 | expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
273 | end
274 | end
275 |
276 | describe "#pundit_reset!" do
277 | it "allows authorize to react to a user change" do
278 | expect(controller.authorize(post)).to be_truthy
279 |
280 | controller.current_user = double
281 | controller.pundit_reset!
282 | expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError)
283 | end
284 |
285 | it "allows policy to react to a user change" do
286 | expect(controller.policy(DummyCurrentUser).user).to be user
287 |
288 | new_user = double("new user")
289 | controller.current_user = new_user
290 | controller.pundit_reset!
291 | expect(controller.policy(DummyCurrentUser).user).to be new_user
292 | end
293 |
294 | it "allows policy scope to react to a user change" do
295 | expect(controller.policy_scope(DummyCurrentUser)).to be user
296 |
297 | new_user = double("new user")
298 | controller.current_user = new_user
299 | controller.pundit_reset!
300 | expect(controller.policy_scope(DummyCurrentUser)).to be new_user
301 | end
302 |
303 | it "resets the pundit context" do
304 | expect(controller.pundit.user).to be(user)
305 |
306 | new_user = double
307 | controller.current_user = new_user
308 | expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user)
309 | end
310 |
311 | it "clears pundit_policy_authorized? flag" do
312 | expect(controller.pundit_policy_authorized?).to be false
313 |
314 | controller.skip_authorization
315 | expect(controller.pundit_policy_authorized?).to be true
316 |
317 | controller.pundit_reset!
318 | expect(controller.pundit_policy_authorized?).to be false
319 | end
320 |
321 | it "clears pundit_policy_scoped? flag" do
322 | expect(controller.pundit_policy_scoped?).to be false
323 |
324 | controller.skip_policy_scope
325 | expect(controller.pundit_policy_scoped?).to be true
326 |
327 | controller.pundit_reset!
328 | expect(controller.pundit_policy_scoped?).to be false
329 | end
330 | end
331 | end
332 |
--------------------------------------------------------------------------------
/spec/generators_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "tmpdir"
5 |
6 | require "rails/generators"
7 | require "generators/pundit/install/install_generator"
8 | require "generators/pundit/policy/policy_generator"
9 |
10 | RSpec.describe "generators" do
11 | before(:all) do
12 | @tmpdir = Dir.mktmpdir
13 |
14 | Dir.chdir(@tmpdir) do
15 | Pundit::Generators::InstallGenerator.new([], { quiet: true }).invoke_all
16 | Pundit::Generators::PolicyGenerator.new(%w[Widget], { quiet: true }).invoke_all
17 |
18 | require "./app/policies/application_policy"
19 | require "./app/policies/widget_policy"
20 | end
21 | end
22 |
23 | after(:all) do
24 | FileUtils.remove_entry(@tmpdir)
25 | end
26 |
27 | describe "WidgetPolicy", type: :policy do
28 | permissions :index?, :show?, :create?, :new?, :update?, :edit?, :destroy? do
29 | it "has safe defaults" do
30 | expect(WidgetPolicy).not_to permit(double("User"), double("Widget"))
31 | end
32 | end
33 |
34 | describe "WidgetPolicy::Scope" do
35 | describe "#resolve" do
36 | it "raises a descriptive error" do
37 | scope = WidgetPolicy::Scope.new(double("User"), double("User.all"))
38 | expect { scope.resolve }.to raise_error(NoMethodError, /WidgetPolicy::Scope/)
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/policies/post_policy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe PostPolicy do
6 | let(:user) { double }
7 | let(:own_post) { double(user: user) }
8 | let(:other_post) { double(user: double) }
9 | subject { described_class }
10 |
11 | permissions :update?, :show? do
12 | it "is successful when all permissions match" do
13 | should permit(user, own_post)
14 | end
15 |
16 | it "fails when any permissions do not match" do
17 | expect do
18 | should permit(user, other_post)
19 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
20 | end
21 |
22 | it "uses the default description if not overridden" do
23 | expect(permit(user, own_post).description).to eq("permit #{user.inspect} and #{own_post.inspect}")
24 | end
25 |
26 | context "when the matcher description is overridden" do
27 | after do
28 | Pundit::RSpec::Matchers.description = nil
29 | end
30 |
31 | it "sets a custom matcher description with a Proc" do
32 | allow(user).to receive(:role).and_return("default_role")
33 | allow(own_post).to receive(:id).and_return(1)
34 |
35 | Pundit::RSpec::Matchers.description = lambda { |user, record|
36 | "permit user with role #{user.role} to access record with ID #{record.id}"
37 | }
38 |
39 | description = permit(user, own_post).description
40 | expect(description).to eq("permit user with role default_role to access record with ID 1")
41 | end
42 |
43 | it "sets a custom matcher description with a string" do
44 | Pundit::RSpec::Matchers.description = "permit user"
45 | expect(permit(user, own_post).description).to eq("permit user")
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/policy_finder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Pundit::PolicyFinder do
6 | let(:user) { double }
7 | let(:post) { Post.new(user) }
8 | let(:comment) { CommentFourFiveSix.new }
9 | let(:article) { Article.new }
10 |
11 | describe "SUFFIX" do
12 | specify { expect(described_class::SUFFIX).to eq "Policy" }
13 | specify { expect(Pundit::SUFFIX).to eq(described_class::SUFFIX) }
14 | end
15 |
16 | describe "#scope" do
17 | subject { described_class.new(post) }
18 |
19 | it "returns a policy scope" do
20 | expect(subject.scope).to eq PostPolicy::Scope
21 | end
22 |
23 | context "policy is nil" do
24 | it "returns nil" do
25 | allow(subject).to receive(:policy).and_return nil
26 | expect(subject.scope).to eq nil
27 | end
28 | end
29 | end
30 |
31 | describe "#policy" do
32 | context "with an instance" do
33 | it "returns the associated policy" do
34 | object = described_class.new(post)
35 |
36 | expect(object.policy).to eq PostPolicy
37 | end
38 | end
39 |
40 | context "with an array of symbols" do
41 | it "returns the associated namespaced policy" do
42 | object = described_class.new(%i[project post])
43 |
44 | expect(object.policy).to eq Project::PostPolicy
45 | end
46 | end
47 |
48 | context "with an array of a symbol and an instance" do
49 | it "returns the associated namespaced policy" do
50 | object = described_class.new([:project, post])
51 |
52 | expect(object.policy).to eq Project::PostPolicy
53 | end
54 | end
55 |
56 | context "with an array of a symbol and a class with a specified policy class" do
57 | it "returns the associated namespaced policy" do
58 | object = described_class.new([:project, Customer::Post])
59 |
60 | expect(object.policy).to eq Project::PostPolicy
61 | end
62 | end
63 |
64 | context "with an array of a symbol and a class with a specified model name" do
65 | it "returns the associated namespaced policy" do
66 | object = described_class.new([:project, CommentsRelation])
67 |
68 | expect(object.policy).to eq Project::CommentPolicy
69 | end
70 | end
71 |
72 | context "with a class" do
73 | it "returns the associated policy" do
74 | object = described_class.new(Post)
75 |
76 | expect(object.policy).to eq PostPolicy
77 | end
78 | end
79 |
80 | context "with a class which has a specified policy class" do
81 | it "returns the associated policy" do
82 | object = described_class.new(Customer::Post)
83 |
84 | expect(object.policy).to eq PostPolicy
85 | end
86 | end
87 |
88 | context "with an instance which has a specified policy class" do
89 | it "returns the associated policy" do
90 | object = described_class.new(Customer::Post.new(user))
91 |
92 | expect(object.policy).to eq PostPolicy
93 | end
94 | end
95 |
96 | context "with a class which has a specified model name" do
97 | it "returns the associated policy" do
98 | object = described_class.new(CommentsRelation)
99 |
100 | expect(object.policy).to eq CommentPolicy
101 | end
102 | end
103 |
104 | context "with an instance which has a specified policy class" do
105 | it "returns the associated policy" do
106 | object = described_class.new(CommentsRelation.new)
107 |
108 | expect(object.policy).to eq CommentPolicy
109 | end
110 | end
111 |
112 | context "with nil" do
113 | it "returns a NilClassPolicy" do
114 | object = described_class.new(nil)
115 |
116 | expect(object.policy).to eq NilClassPolicy
117 | end
118 | end
119 |
120 | context "with a class that doesn't have an associated policy" do
121 | it "returns nil" do
122 | object = described_class.new(Foo)
123 |
124 | expect(object.policy).to eq nil
125 | end
126 | end
127 | end
128 |
129 | describe "#scope!" do
130 | context "@object is nil" do
131 | subject { described_class.new(nil) }
132 |
133 | it "returns the NilClass policy's scope class" do
134 | expect(subject.scope!).to eq NilClassPolicy::Scope
135 | end
136 | end
137 |
138 | context "@object is defined" do
139 | subject { described_class.new(post) }
140 |
141 | it "returns the scope" do
142 | expect(subject.scope!).to eq PostPolicy::Scope
143 | end
144 | end
145 | end
146 |
147 | describe "#param_key" do
148 | context "object responds to model_name" do
149 | subject { described_class.new(comment) }
150 |
151 | it "returns the param_key" do
152 | expect(subject.object).to respond_to(:model_name)
153 | expect(subject.param_key).to eq "comment_four_five_six"
154 | end
155 | end
156 |
157 | context "object is a class" do
158 | subject { described_class.new(Article) }
159 |
160 | it "returns the param_key" do
161 | expect(subject.object).not_to respond_to(:model_name)
162 | expect(subject.object).to be_a Class
163 | expect(subject.param_key).to eq "article"
164 | end
165 | end
166 |
167 | context "object is an instance of a class" do
168 | subject { described_class.new(article) }
169 |
170 | it "returns the param_key" do
171 | expect(subject.object).not_to respond_to(:model_name)
172 | expect(subject.object).not_to be_a Class
173 | expect(subject.object).to be_an_instance_of Article
174 |
175 | expect(subject.param_key).to eq "article"
176 | end
177 | end
178 |
179 | context "object is an array" do
180 | subject { described_class.new([:project, article]) }
181 |
182 | it "returns the param_key for the last element of the array" do
183 | expect(subject.object).not_to respond_to(:model_name)
184 | expect(subject.object).not_to be_a Class
185 | expect(subject.object).to be_an_instance_of Array
186 |
187 | expect(subject.param_key).to eq "article"
188 | end
189 | end
190 | end
191 | end
192 |
--------------------------------------------------------------------------------
/spec/pundit/helper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Pundit::Helper do
6 | let(:user) { double }
7 | let(:controller) { Controller.new(user, "update", double) }
8 | let(:view) { Controller::View.new(controller) }
9 |
10 | describe "#policy_scope" do
11 | it "doesn't flip pundit_policy_scoped?" do
12 | scoped = view.policy_scope(Post)
13 |
14 | expect(scoped).to be(Post.published)
15 | expect(controller).not_to be_pundit_policy_scoped
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/pundit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Pundit do
6 | let(:user) { double }
7 | let(:post) { Post.new(user) }
8 | let(:customer_post) { Customer::Post.new(user) }
9 | let(:post_four_five_six) { PostFourFiveSix.new(user) }
10 | let(:comment) { Comment.new }
11 | let(:comment_four_five_six) { CommentFourFiveSix.new }
12 | let(:article) { Article.new }
13 | let(:artificial_blog) { ArtificialBlog.new }
14 | let(:article_tag) { ArticleTag.new }
15 | let(:comments_relation) { CommentsRelation.new(empty: false) }
16 | let(:empty_comments_relation) { CommentsRelation.new(empty: true) }
17 | let(:tag_four_five_six) { ProjectOneTwoThree::TagFourFiveSix.new(user) }
18 | let(:avatar_four_five_six) { ProjectOneTwoThree::AvatarFourFiveSix.new }
19 | let(:wiki) { Wiki.new }
20 |
21 | describe ".authorize" do
22 | it "infers the policy and authorizes based on it" do
23 | expect(Pundit.authorize(user, post, :update?)).to be_truthy
24 | end
25 |
26 | it "returns the record on successful authorization" do
27 | expect(Pundit.authorize(user, post, :update?)).to eq(post)
28 | end
29 |
30 | it "returns the record when passed record with namespace " do
31 | expect(Pundit.authorize(user, [:project, comment], :update?)).to eq(comment)
32 | end
33 |
34 | it "returns the record when passed record with nested namespace " do
35 | expect(Pundit.authorize(user, [:project, :admin, comment], :update?)).to eq(comment)
36 | end
37 |
38 | it "returns the policy name symbol when passed record with headless policy" do
39 | expect(Pundit.authorize(user, :publication, :create?)).to eq(:publication)
40 | end
41 |
42 | it "returns the class when passed record not a particular instance" do
43 | expect(Pundit.authorize(user, Post, :show?)).to eq(Post)
44 | end
45 |
46 | it "works with anonymous class policies" do
47 | expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy
48 | expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
49 | end
50 |
51 | it "raises an error with the policy, query and record" do
52 | # rubocop:disable Style/MultilineBlockChain
53 | expect do
54 | Pundit.authorize(user, post, :destroy?)
55 | end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? this Post") do |error|
56 | expect(error.query).to eq :destroy?
57 | expect(error.record).to eq post
58 | expect(error.policy).to have_attributes(
59 | user: user,
60 | record: post
61 | )
62 | expect(error.policy).to be_a(PostPolicy)
63 | end
64 | # rubocop:enable Style/MultilineBlockChain
65 | end
66 |
67 | it "raises an error with the policy, query and record when the record is namespaced" do
68 | # rubocop:disable Style/MultilineBlockChain
69 | expect do
70 | Pundit.authorize(user, [:project, :admin, comment], :destroy?)
71 | end.to raise_error(Pundit::NotAuthorizedError,
72 | "not allowed to Project::Admin::CommentPolicy#destroy? this Comment") do |error|
73 | expect(error.query).to eq :destroy?
74 | expect(error.record).to eq comment
75 | expect(error.policy).to have_attributes(
76 | user: user,
77 | record: comment
78 | )
79 | expect(error.policy).to be_a(Project::Admin::CommentPolicy)
80 | end
81 | # rubocop:enable Style/MultilineBlockChain
82 | end
83 |
84 | it "raises an error with the policy, query and the class name when a Class is given" do
85 | # rubocop:disable Style/MultilineBlockChain
86 | expect do
87 | Pundit.authorize(user, Post, :destroy?)
88 | end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? Post") do |error|
89 | expect(error.query).to eq :destroy?
90 | expect(error.record).to eq Post
91 | expect(error.policy).to have_attributes(
92 | user: user,
93 | record: Post
94 | )
95 | expect(error.policy).to be_a(PostPolicy)
96 | end
97 | # rubocop:enable Style/MultilineBlockChain
98 | end
99 |
100 | it "raises an error with a invalid policy constructor" do
101 | expect do
102 | Pundit.authorize(user, wiki, :update?)
103 | end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called")
104 | end
105 |
106 | context "when passed a policy class" do
107 | it "uses the passed policy class" do
108 | expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy
109 | end
110 |
111 | # This is documenting past behaviour.
112 | it "doesn't cache the policy class" do
113 | cache = {}
114 |
115 | expect do
116 | Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
117 | Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
118 | end.to change { PublicationPolicy.instances }.by(2)
119 | end
120 | end
121 |
122 | context "when passed a policy class while simultaenously passing a namespace" do
123 | it "uses the passed policy class" do
124 | expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original
125 | expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy
126 | end
127 | end
128 |
129 | context "when passed an explicit cache" do
130 | it "uses the hash assignment interface on the cache" do
131 | custom_cache = CustomCache.new
132 |
133 | Pundit.authorize(user, post, :update?, cache: custom_cache)
134 |
135 | expect(custom_cache.to_h).to match({
136 | post => kind_of(PostPolicy)
137 | })
138 | end
139 | end
140 | end
141 |
142 | describe ".policy_scope" do
143 | it "returns an instantiated policy scope given a plain model class" do
144 | expect(Pundit.policy_scope(user, Post)).to eq :published
145 | end
146 |
147 | it "returns an instantiated policy scope given an active model class" do
148 | expect(Pundit.policy_scope(user, Comment)).to eq CommentScope.new(Comment)
149 | end
150 |
151 | it "returns an instantiated policy scope given an active record relation" do
152 | expect(Pundit.policy_scope(user, comments_relation)).to eq CommentScope.new(comments_relation)
153 | end
154 |
155 | it "returns an instantiated policy scope given an empty active record relation" do
156 | expect(Pundit.policy_scope(user, empty_comments_relation)).to eq CommentScope.new(empty_comments_relation)
157 | end
158 |
159 | it "returns an instantiated policy scope given an array of a symbol and plain model class" do
160 | expect(Pundit.policy_scope(user, [:project, Post])).to eq :read
161 | end
162 |
163 | it "returns an instantiated policy scope given an array of a symbol and active model class" do
164 | expect(Pundit.policy_scope(user, [:project, Comment])).to eq Comment
165 | end
166 |
167 | it "returns nil if the given policy scope can't be found" do
168 | expect(Pundit.policy_scope(user, Article)).to be_nil
169 | end
170 |
171 | it "raises an exception if nil object given" do
172 | expect { Pundit.policy_scope(user, nil) }.to raise_error(Pundit::NotDefinedError)
173 | end
174 |
175 | it "raises an error with a invalid policy scope constructor" do
176 | expect do
177 | Pundit.policy_scope(user, Wiki)
178 | end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called")
179 | end
180 |
181 | it "raises an original error with a policy scope that contains error" do
182 | expect do
183 | Pundit.policy_scope(user, DefaultScopeContainsError)
184 | end.to raise_error(RuntimeError, "This is an arbitrary error that should bubble up")
185 | end
186 | end
187 |
188 | describe ".policy_scope!" do
189 | it "returns an instantiated policy scope given a plain model class" do
190 | expect(Pundit.policy_scope!(user, Post)).to eq :published
191 | end
192 |
193 | it "returns an instantiated policy scope given an active model class" do
194 | expect(Pundit.policy_scope!(user, Comment)).to eq CommentScope.new(Comment)
195 | end
196 |
197 | it "throws an exception if the given policy scope can't be found" do
198 | expect { Pundit.policy_scope!(user, Article) }.to raise_error(Pundit::NotDefinedError)
199 | end
200 |
201 | it "throws an exception if the given policy scope can't be found" do
202 | expect { Pundit.policy_scope!(user, ArticleTag) }.to raise_error(Pundit::NotDefinedError)
203 | end
204 |
205 | it "throws an exception if the given policy scope is nil" do
206 | expect do
207 | Pundit.policy_scope!(user, nil)
208 | end.to raise_error(Pundit::NotDefinedError, "Cannot scope NilClass")
209 | end
210 |
211 | it "returns an instantiated policy scope given an array of a symbol and plain model class" do
212 | expect(Pundit.policy_scope!(user, [:project, Post])).to eq :read
213 | end
214 |
215 | it "returns an instantiated policy scope given an array of a symbol and active model class" do
216 | expect(Pundit.policy_scope!(user, [:project, Comment])).to eq Comment
217 | end
218 |
219 | it "raises an error with a invalid policy scope constructor" do
220 | expect do
221 | Pundit.policy_scope(user, Wiki)
222 | end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called")
223 | end
224 | end
225 |
226 | describe ".policy" do
227 | it "returns an instantiated policy given a plain model instance" do
228 | policy = Pundit.policy(user, post)
229 | expect(policy.user).to eq user
230 | expect(policy.post).to eq post
231 | end
232 |
233 | it "returns an instantiated policy given an active model instance" do
234 | policy = Pundit.policy(user, comment)
235 | expect(policy.user).to eq user
236 | expect(policy.comment).to eq comment
237 | end
238 |
239 | it "returns an instantiated policy given a plain model class" do
240 | policy = Pundit.policy(user, Post)
241 | expect(policy.user).to eq user
242 | expect(policy.post).to eq Post
243 | end
244 |
245 | it "returns an instantiated policy given an active model class" do
246 | policy = Pundit.policy(user, Comment)
247 | expect(policy.user).to eq user
248 | expect(policy.comment).to eq Comment
249 | end
250 |
251 | it "returns an instantiated policy given a symbol" do
252 | policy = Pundit.policy(user, :criteria)
253 | expect(policy.class).to eq CriteriaPolicy
254 | expect(policy.user).to eq user
255 | expect(policy.criteria).to eq :criteria
256 | end
257 |
258 | it "returns an instantiated policy given an array of symbols" do
259 | policy = Pundit.policy(user, %i[project criteria])
260 | expect(policy.class).to eq Project::CriteriaPolicy
261 | expect(policy.user).to eq user
262 | expect(policy.criteria).to eq :criteria
263 | end
264 |
265 | it "returns an instantiated policy given an array of a symbol and plain model instance" do
266 | policy = Pundit.policy(user, [:project, post])
267 | expect(policy.class).to eq Project::PostPolicy
268 | expect(policy.user).to eq user
269 | expect(policy.post).to eq post
270 | end
271 |
272 | it "returns an instantiated policy given an array of a symbol and a model instance with policy_class override" do
273 | policy = Pundit.policy(user, [:project, customer_post])
274 | expect(policy.class).to eq Project::PostPolicy
275 | expect(policy.user).to eq user
276 | expect(policy.post).to eq customer_post
277 | end
278 |
279 | it "returns an instantiated policy given an array of a symbol and an active model instance" do
280 | policy = Pundit.policy(user, [:project, comment])
281 | expect(policy.class).to eq Project::CommentPolicy
282 | expect(policy.user).to eq user
283 | expect(policy.comment).to eq comment
284 | end
285 |
286 | it "returns an instantiated policy given an array of a symbol and a plain model class" do
287 | policy = Pundit.policy(user, [:project, Post])
288 | expect(policy.class).to eq Project::PostPolicy
289 | expect(policy.user).to eq user
290 | expect(policy.post).to eq Post
291 | end
292 |
293 | it "raises an error with a invalid policy constructor" do
294 | expect do
295 | Pundit.policy(user, Wiki)
296 | end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called")
297 | end
298 |
299 | it "returns an instantiated policy given an array of a symbol and an active model class" do
300 | policy = Pundit.policy(user, [:project, Comment])
301 | expect(policy.class).to eq Project::CommentPolicy
302 | expect(policy.user).to eq user
303 | expect(policy.comment).to eq Comment
304 | end
305 |
306 | it "returns an instantiated policy given an array of a symbol and a class with policy_class override" do
307 | policy = Pundit.policy(user, [:project, Customer::Post])
308 | expect(policy.class).to eq Project::PostPolicy
309 | expect(policy.user).to eq user
310 | expect(policy.post).to eq Customer::Post
311 | end
312 |
313 | it "returns correct policy class for an array of a multi-word symbols" do
314 | policy = Pundit.policy(user, %i[project_one_two_three criteria_four_five_six])
315 | expect(policy.class).to eq ProjectOneTwoThree::CriteriaFourFiveSixPolicy
316 | end
317 |
318 | it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model instance" do
319 | policy = Pundit.policy(user, [:project_one_two_three, post_four_five_six])
320 | expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy
321 | end
322 |
323 | it "returns correct policy class for an array of a multi-word symbol and a multi-word active model instance" do
324 | policy = Pundit.policy(user, [:project_one_two_three, comment_four_five_six])
325 | expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy
326 | end
327 |
328 | it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model class" do
329 | policy = Pundit.policy(user, [:project_one_two_three, PostFourFiveSix])
330 | expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy
331 | end
332 |
333 | it "returns correct policy class for an array of a multi-word symbol and a multi-word active model class" do
334 | policy = Pundit.policy(user, [:project_one_two_three, CommentFourFiveSix])
335 | expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy
336 | end
337 |
338 | it "returns correct policy class for a multi-word scoped plain model class" do
339 | policy = Pundit.policy(user, ProjectOneTwoThree::TagFourFiveSix)
340 | expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy
341 | end
342 |
343 | it "returns correct policy class for a multi-word scoped plain model instance" do
344 | policy = Pundit.policy(user, tag_four_five_six)
345 | expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy
346 | end
347 |
348 | it "returns correct policy class for a multi-word scoped active model class" do
349 | policy = Pundit.policy(user, ProjectOneTwoThree::AvatarFourFiveSix)
350 | expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy
351 | end
352 |
353 | it "returns correct policy class for a multi-word scoped active model instance" do
354 | policy = Pundit.policy(user, avatar_four_five_six)
355 | expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy
356 | end
357 |
358 | it "returns nil if the given policy can't be found" do
359 | expect(Pundit.policy(user, article)).to be_nil
360 | expect(Pundit.policy(user, Article)).to be_nil
361 | end
362 |
363 | it "returns the specified NilClassPolicy for nil" do
364 | expect(Pundit.policy(user, nil)).to be_a NilClassPolicy
365 | end
366 |
367 | describe "with .policy_class set on the model" do
368 | it "returns an instantiated policy given a plain model instance" do
369 | policy = Pundit.policy(user, artificial_blog)
370 | expect(policy.user).to eq user
371 | expect(policy.blog).to eq artificial_blog
372 | end
373 |
374 | it "returns an instantiated policy given a plain model class" do
375 | policy = Pundit.policy(user, ArtificialBlog)
376 | expect(policy.user).to eq user
377 | expect(policy.blog).to eq ArtificialBlog
378 | end
379 |
380 | it "returns an instantiated policy given a plain model instance providing an anonymous class" do
381 | policy = Pundit.policy(user, article_tag)
382 | expect(policy.user).to eq user
383 | expect(policy.tag).to eq article_tag
384 | end
385 |
386 | it "returns an instantiated policy given a plain model class providing an anonymous class" do
387 | policy = Pundit.policy(user, ArticleTag)
388 | expect(policy.user).to eq user
389 | expect(policy.tag).to eq ArticleTag
390 | end
391 | end
392 | end
393 |
394 | describe ".policy!" do
395 | it "returns an instantiated policy given a plain model instance" do
396 | policy = Pundit.policy!(user, post)
397 | expect(policy.user).to eq user
398 | expect(policy.post).to eq post
399 | end
400 |
401 | it "returns an instantiated policy given an active model instance" do
402 | policy = Pundit.policy!(user, comment)
403 | expect(policy.user).to eq user
404 | expect(policy.comment).to eq comment
405 | end
406 |
407 | it "returns an instantiated policy given a plain model class" do
408 | policy = Pundit.policy!(user, Post)
409 | expect(policy.user).to eq user
410 | expect(policy.post).to eq Post
411 | end
412 |
413 | it "returns an instantiated policy given an active model class" do
414 | policy = Pundit.policy!(user, Comment)
415 | expect(policy.user).to eq user
416 | expect(policy.comment).to eq Comment
417 | end
418 |
419 | it "returns an instantiated policy given a symbol" do
420 | policy = Pundit.policy!(user, :criteria)
421 | expect(policy.class).to eq CriteriaPolicy
422 | expect(policy.user).to eq user
423 | expect(policy.criteria).to eq :criteria
424 | end
425 |
426 | it "returns an instantiated policy given an array of symbols" do
427 | policy = Pundit.policy!(user, %i[project criteria])
428 | expect(policy.class).to eq Project::CriteriaPolicy
429 | expect(policy.user).to eq user
430 | expect(policy.criteria).to eq :criteria
431 | end
432 |
433 | it "throws an exception if the given policy can't be found" do
434 | expect { Pundit.policy!(user, article) }.to raise_error(Pundit::NotDefinedError)
435 | expect { Pundit.policy!(user, Article) }.to raise_error(Pundit::NotDefinedError)
436 | end
437 |
438 | it "returns the specified NilClassPolicy for nil" do
439 | expect(Pundit.policy!(user, nil)).to be_a NilClassPolicy
440 | end
441 |
442 | it "raises an error with a invalid policy constructor" do
443 | expect do
444 | Pundit.policy(user, Wiki)
445 | end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called")
446 | end
447 | end
448 |
449 | describe ".included" do
450 | it "includes Authorization module" do
451 | klass = Class.new
452 |
453 | expect do
454 | klass.include Pundit
455 | end.to output.to_stderr
456 |
457 | expect(klass).to include Pundit::Authorization
458 | end
459 |
460 | it "warns about deprecation" do
461 | klass = Class.new
462 | expect do
463 | klass.include Pundit
464 | end.to output(a_string_starting_with("'include Pundit' is deprecated")).to_stderr
465 | end
466 | end
467 |
468 | describe "Pundit::NotAuthorizedError" do
469 | it "can be initialized with a string as message" do
470 | error = Pundit::NotAuthorizedError.new("must be logged in")
471 | expect(error.message).to eq "must be logged in"
472 | end
473 | end
474 | end
475 |
--------------------------------------------------------------------------------
/spec/rspec_dsl_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe "Pundit RSpec DSL" do
6 | include Pundit::RSpec::PolicyExampleGroup
7 |
8 | let(:fake_rspec) do
9 | double = class_double(RSpec::ExampleGroups)
10 | double.extend(::Pundit::RSpec::DSL)
11 | double
12 | end
13 | let(:block) { proc { "block content" } }
14 |
15 | let(:user) { double }
16 | let(:other_user) { double }
17 | let(:post) { Post.new(user) }
18 | let(:policy) { PostPolicy }
19 |
20 | it "calls describe with the correct metadata and without :focus" do
21 | expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) }
22 | expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
23 | expect(block.call).to eq("block content")
24 | end
25 |
26 | fake_rspec.permissions(:item1, :item2, &block)
27 | end
28 |
29 | it "calls describe with the correct metadata and with :focus" do
30 | expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true }
31 | expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
32 | expect(block.call).to eq("block content")
33 | end
34 |
35 | fake_rspec.permissions(:item1, :item2, :focus, &block)
36 | end
37 |
38 | describe "#permit" do
39 | context "when not appropriately wrapped in permissions" do
40 | it "raises a descriptive error" do
41 | expect do
42 | expect(policy).to permit(user, post)
43 | end.to raise_error(KeyError, <<~MSG.strip)
44 | No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
45 | MSG
46 | end
47 | end
48 |
49 | permissions :edit?, :update? do
50 | it "succeeds when action is permitted" do
51 | expect(policy).to permit(user, post)
52 | end
53 |
54 | context "when it fails" do
55 | it "fails with a descriptive error message" do
56 | expect do
57 | expect(policy).to permit(other_user, post)
58 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
59 | Expected PostPolicy to grant edit? and update? on Post but edit? and update? were not granted
60 | MSG
61 | end
62 | end
63 |
64 | context "when negated" do
65 | it "succeeds when action is not permitted" do
66 | expect(policy).not_to permit(other_user, post)
67 | end
68 |
69 | context "when it fails" do
70 | it "fails with a descriptive error message" do
71 | expect do
72 | expect(policy).not_to permit(user, post)
73 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
74 | Expected PostPolicy not to grant edit? and update? on Post but edit? and update? were granted
75 | MSG
76 | end
77 | end
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/spec/simple_cov_check_action_formatter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "simplecov"
4 | require "json"
5 |
6 | class SimpleCovCheckActionFormatter
7 | SourceFile = Data.define(:source_file) do
8 | def covered_strength = source_file.covered_strength
9 | def covered_percent = source_file.covered_percent
10 |
11 | def to_json(*args)
12 | {
13 | filename: source_file.filename,
14 | covered_percent: covered_percent.nan? ? 0.0 : covered_percent,
15 | coverage: source_file.coverage_data,
16 | covered_strength: covered_strength.nan? ? 0.0 : covered_strength,
17 | covered_lines: source_file.covered_lines.count,
18 | lines_of_code: source_file.lines_of_code
19 | }.to_json(*args)
20 | end
21 | end
22 |
23 | Result = Data.define(:result) do
24 | def included?(source_file) = result.filenames.include?(source_file.filename)
25 |
26 | def files
27 | result.files.filter_map do |source_file|
28 | next unless result.filenames.include? source_file.filename
29 |
30 | SourceFile.new(source_file)
31 | end
32 | end
33 |
34 | def to_json(*args) # rubocop:disable Metrics/AbcSize
35 | {
36 | timestamp: result.created_at.to_i,
37 | command_name: result.command_name,
38 | files: files,
39 | metrics: {
40 | covered_percent: result.covered_percent,
41 | covered_strength: result.covered_strength.nan? ? 0.0 : result.covered_strength,
42 | covered_lines: result.covered_lines,
43 | total_lines: result.total_lines
44 | }
45 | }.to_json(*args)
46 | end
47 | end
48 |
49 | FormatterWithOptions = Data.define(:formatter) do
50 | def new = formatter
51 | end
52 |
53 | class << self
54 | def with_options(...)
55 | FormatterWithOptions.new(new(...))
56 | end
57 | end
58 |
59 | def initialize(output_filename: "coverage.json", output_directory: SimpleCov.coverage_path)
60 | @output_filename = output_filename
61 | @output_directory = output_directory
62 | end
63 |
64 | attr_reader :output_filename, :output_directory
65 |
66 | def output_filepath = File.join(output_directory, output_filename)
67 |
68 | def format(result_data)
69 | result = Result.new(result_data)
70 | json = JSON.generate(result)
71 | File.write(output_filepath, json)
72 | puts output_message(result_data)
73 | json
74 | end
75 |
76 | def output_message(result)
77 | "Coverage report generated for #{result.command_name} to #{output_filepath}. #{result.covered_lines} / #{result.total_lines} LOC (#{result.covered_percent.round(2)}%) covered." # rubocop:disable Layout/LineLength
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if ENV["COVERAGE"]
4 | require "simplecov"
5 | require "simplecov_json_formatter"
6 | require_relative "simple_cov_check_action_formatter"
7 | SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
8 | SimpleCov::Formatter::HTMLFormatter,
9 | SimpleCov::Formatter::JSONFormatter,
10 | SimpleCovCheckActionFormatter.with_options(
11 | output_filename: "simplecov-check-action.json"
12 | )
13 | ])
14 | SimpleCov.start do
15 | add_filter "/spec/"
16 | enable_coverage :branch
17 | primary_coverage :branch
18 | end
19 | end
20 |
21 | # @see https://github.com/rails/rails/issues/54260
22 | require "logger" if RUBY_ENGINE == "jruby" && RUBY_ENGINE_VERSION.start_with?("9.3")
23 |
24 | require "pundit"
25 | require "pundit/rspec"
26 | require "active_model/naming"
27 |
28 | # Load all supporting files: models, policies, etc.
29 | require "zeitwerk"
30 | loader = Zeitwerk::Loader.new
31 | loader.push_dir(File.expand_path("support/models", __dir__))
32 | loader.push_dir(File.expand_path("support/policies", __dir__))
33 | loader.push_dir(File.expand_path("support/lib", __dir__))
34 | loader.setup
35 | loader.eager_load
36 |
--------------------------------------------------------------------------------
/spec/support/lib/controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Controller
4 | attr_accessor :current_user
5 | attr_reader :action_name, :params
6 |
7 | class View
8 | def initialize(controller)
9 | @controller = controller
10 | end
11 |
12 | attr_reader :controller
13 | end
14 |
15 | class << self
16 | def helper(mod)
17 | View.include(mod)
18 | end
19 |
20 | def helper_method(method)
21 | View.class_eval <<-RUBY, __FILE__, __LINE__ + 1
22 | def #{method}(*args, **kwargs, &block)
23 | controller.send(:#{method}, *args, **kwargs, &block)
24 | end
25 | RUBY
26 | end
27 | end
28 |
29 | include Pundit::Authorization
30 | # Mark protected methods public so they may be called in test
31 | public(*Pundit::Authorization.protected_instance_methods)
32 |
33 | def initialize(current_user, action_name, params)
34 | @current_user = current_user
35 | @action_name = action_name
36 | @params = params
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/support/lib/custom_cache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CustomCache
4 | def initialize
5 | @store = {}
6 | end
7 |
8 | def to_h
9 | @store
10 | end
11 |
12 | def [](key)
13 | @store[key]
14 | end
15 |
16 | def []=(key, value)
17 | @store[key] = value
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/support/lib/instance_tracking.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module InstanceTracking
4 | module ClassMethods
5 | def instances
6 | @instances || 0
7 | end
8 |
9 | attr_writer :instances
10 | end
11 |
12 | def self.prepended(other)
13 | other.extend(ClassMethods)
14 | end
15 |
16 | def initialize(*args, **kwargs, &block)
17 | self.class.instances += 1
18 | super(*args, **kwargs, &block)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/support/models/article.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Article
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/models/article_tag.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ArticleTag
4 | def self.policy_class
5 | ArticleTagOtherNamePolicy
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/models/artificial_blog.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ArtificialBlog < Blog
4 | def self.policy_class
5 | BlogPolicy
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/models/blog.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Blog
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/models/comment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Comment
4 | extend ActiveModel::Naming
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/models/comment_four_five_six.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CommentFourFiveSix
4 | extend ActiveModel::Naming
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/models/comment_scope.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CommentScope
4 | attr_reader :original_object
5 |
6 | def initialize(original_object)
7 | @original_object = original_object
8 | end
9 |
10 | def ==(other)
11 | original_object == other.original_object
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/models/comments_relation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CommentsRelation
4 | def initialize(empty: false)
5 | @empty = empty
6 | end
7 |
8 | def blank?
9 | @empty
10 | end
11 |
12 | def self.model_name
13 | Comment.model_name
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/support/models/customer/post.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Customer
4 | class Post < ::Post
5 | extend ActiveModel::Naming
6 |
7 | def self.policy_class
8 | PostPolicy
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/models/default_scope_contains_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DefaultScopeContainsError
4 | def self.all; end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/models/dummy_current_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DummyCurrentUser
4 | def update?
5 | user
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/models/foo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Foo
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/models/post.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Post
4 | def initialize(user = nil)
5 | @user = user
6 | end
7 |
8 | attr_reader :user
9 |
10 | def self.published
11 | :published
12 | end
13 |
14 | def self.read
15 | :read
16 | end
17 |
18 | def to_s
19 | "Post"
20 | end
21 |
22 | def inspect
23 | "#"
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/support/models/post_four_five_six.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PostFourFiveSix
4 | def initialize(user)
5 | @user = user
6 | end
7 |
8 | attr_reader(:user)
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/models/project_one_two_three/avatar_four_five_six.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class AvatarFourFiveSix
5 | extend ActiveModel::Naming
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/models/project_one_two_three/tag_four_five_six.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class TagFourFiveSix
5 | def initialize(user)
6 | @user = user
7 | end
8 |
9 | attr_reader(:user)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/models/wiki.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Wiki
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/policies/article_tag_other_name_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ArticleTagOtherNamePolicy < BasePolicy
4 | def show?
5 | true
6 | end
7 |
8 | def destroy?
9 | false
10 | end
11 |
12 | alias tag record
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/policies/base_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BasePolicy
4 | prepend InstanceTracking
5 |
6 | class BaseScope
7 | prepend InstanceTracking
8 |
9 | def initialize(user, scope)
10 | @user = user
11 | @scope = scope
12 | end
13 |
14 | attr_reader :user, :scope
15 | end
16 |
17 | def initialize(user, record)
18 | @user = user
19 | @record = record
20 | end
21 |
22 | attr_reader :user, :record
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/policies/blog_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BlogPolicy < BasePolicy
4 | alias blog record
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/policies/comment_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CommentPolicy < BasePolicy
4 | class Scope < BaseScope
5 | def resolve
6 | CommentScope.new(scope)
7 | end
8 | end
9 |
10 | alias comment record
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/policies/criteria_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CriteriaPolicy < BasePolicy
4 | alias criteria record
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/policies/default_scope_contains_error_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DefaultScopeContainsErrorPolicy < BasePolicy
4 | class Scope < BaseScope
5 | def resolve
6 | # deliberate wrong usage of the method
7 | raise "This is an arbitrary error that should bubble up"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/support/policies/denier_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DenierPolicy < BasePolicy
4 | def update?
5 | false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/policies/dummy_current_user_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DummyCurrentUserPolicy < BasePolicy
4 | class Scope < BasePolicy::BaseScope
5 | def resolve
6 | user
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/policies/nil_class_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NilClassPolicy < BasePolicy
4 | class Scope
5 | def initialize(*)
6 | raise Pundit::NotDefinedError, "Cannot scope NilClass"
7 | end
8 | end
9 |
10 | def show?
11 | false
12 | end
13 |
14 | def destroy?
15 | false
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/support/policies/post_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PostPolicy < BasePolicy
4 | class Scope < BaseScope
5 | def resolve
6 | scope.published
7 | end
8 | end
9 |
10 | alias post record
11 |
12 | def update?
13 | post.user == user
14 | end
15 | alias edit? update?
16 |
17 | def destroy?
18 | false
19 | end
20 |
21 | def show?
22 | true
23 | end
24 |
25 | def permitted_attributes
26 | if post.user == user
27 | %i[title votes]
28 | else
29 | [:votes]
30 | end
31 | end
32 |
33 | def permitted_attributes_for_revise
34 | [:body]
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/support/policies/project/admin/comment_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Project
4 | module Admin
5 | class CommentPolicy < BasePolicy
6 | def update?
7 | true
8 | end
9 |
10 | def destroy?
11 | false
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/support/policies/project/comment_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Project
4 | class CommentPolicy < BasePolicy
5 | class Scope < BaseScope
6 | def resolve
7 | scope
8 | end
9 | end
10 |
11 | def update?
12 | true
13 | end
14 |
15 | alias comment record
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/support/policies/project/criteria_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Project
4 | class CriteriaPolicy < BasePolicy
5 | alias criteria record
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/policies/project/post_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Project
4 | class PostPolicy < BasePolicy
5 | class Scope < BaseScope
6 | def resolve
7 | scope.read
8 | end
9 | end
10 |
11 | alias post record
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class AvatarFourFiveSixPolicy < BasePolicy
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class CommentFourFiveSixPolicy < BasePolicy
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class CriteriaFourFiveSixPolicy < BasePolicy
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class PostFourFiveSixPolicy < BasePolicy
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ProjectOneTwoThree
4 | class TagFourFiveSixPolicy < BasePolicy
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/policies/publication_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PublicationPolicy < BasePolicy
4 | class Scope < BaseScope
5 | def resolve
6 | scope.published
7 | end
8 | end
9 |
10 | def create?
11 | true
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/policies/wiki_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class WikiPolicy
4 | class Scope
5 | # deliberate typo method
6 | def initalize; end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------