Repository: varvet/pundit Branch: main Commit: 06318683c960 Files: 92 Total size: 132.4 KB Directory structure: gitextract_wfsck2t6/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE/ │ │ └── gem_release_template.md │ ├── pull_request_template.md │ └── workflows/ │ ├── main.yml │ └── push_gem.yml ├── .gitignore ├── .rubocop_ignore_git.yml ├── .standard.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── bin/ │ ├── console │ └── setup ├── config/ │ └── rubocop-rspec.yml ├── lib/ │ ├── generators/ │ │ ├── pundit/ │ │ │ ├── install/ │ │ │ │ ├── USAGE │ │ │ │ ├── install_generator.rb │ │ │ │ └── templates/ │ │ │ │ └── application_policy.rb.tt │ │ │ └── policy/ │ │ │ ├── USAGE │ │ │ ├── policy_generator.rb │ │ │ └── templates/ │ │ │ └── policy.rb.tt │ │ ├── rspec/ │ │ │ ├── policy_generator.rb │ │ │ └── templates/ │ │ │ └── policy_spec.rb.tt │ │ └── test_unit/ │ │ ├── policy_generator.rb │ │ └── templates/ │ │ └── policy_test.rb.tt │ ├── pundit/ │ │ ├── authorization.rb │ │ ├── cache_store/ │ │ │ ├── legacy_store.rb │ │ │ └── null_store.rb │ │ ├── cache_store.rb │ │ ├── context.rb │ │ ├── error.rb │ │ ├── helper.rb │ │ ├── policy_finder.rb │ │ ├── railtie.rb │ │ ├── rspec.rb │ │ └── version.rb │ └── pundit.rb ├── pundit.gemspec └── spec/ ├── authorization_spec.rb ├── generators_spec.rb ├── policies/ │ └── post_policy_spec.rb ├── policy_finder_spec.rb ├── pundit/ │ └── helper_spec.rb ├── pundit_spec.rb ├── rspec_dsl_spec.rb ├── spec_helper.rb └── support/ ├── lib/ │ ├── controller.rb │ ├── custom_cache.rb │ └── instance_tracking.rb ├── models/ │ ├── article.rb │ ├── article_tag.rb │ ├── artificial_blog.rb │ ├── blog.rb │ ├── comment.rb │ ├── comment_four_five_six.rb │ ├── comment_scope.rb │ ├── comments_relation.rb │ ├── customer/ │ │ └── post.rb │ ├── default_scope_contains_error.rb │ ├── dummy_current_user.rb │ ├── foo.rb │ ├── post.rb │ ├── post_four_five_six.rb │ ├── project_one_two_three/ │ │ ├── avatar_four_five_six.rb │ │ └── tag_four_five_six.rb │ └── wiki.rb └── policies/ ├── article_tag_other_name_policy.rb ├── base_policy.rb ├── blog_policy.rb ├── comment_policy.rb ├── criteria_policy.rb ├── default_scope_contains_error_policy.rb ├── dummy_current_user_policy.rb ├── nil_class_policy.rb ├── post_policy.rb ├── project/ │ ├── admin/ │ │ └── comment_policy.rb │ ├── comment_policy.rb │ ├── criteria_policy.rb │ └── post_policy.rb ├── project_one_two_three/ │ ├── avatar_four_five_six_policy.rb │ ├── comment_four_five_six_policy.rb │ ├── criteria_four_five_six_policy.rb │ ├── post_four_five_six_policy.rb │ └── tag_four_five_six_policy.rb ├── publication_policy.rb └── wiki_policy.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a bug report to report a problem title: '' labels: problem assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps or runnable code to reproduce the problem. **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea title: '' labels: ['feature request'] assignees: '' --- **Please consider** - Could this feature break backwards-compatibility? - Could this feature benefit the many who use Pundit? - Could this feature be useful in _most_ projects that use Pundit? - Would this feature require Rails? - Am I open to creating a Pull Request with the necessary changes? **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of how you'd like to approach solving the problem. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context. Ex. if you've solved this problem in your own projects already, how that worked, and why the feature should be moved and maintained in Pundit instead. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE/gem_release_template.md ================================================ ## To do - [ ] Make changes: - [ ] Bump `Pundit::VERSION` in `lib/pundit/version.rb`. - [ ] Update `CHANGELOG.md`. - [ ] Open pull request 🚀 and merge it. - [ ] Run [push gem](https://github.com/varvet/pundit/actions/workflows/push_gem.yml) GitHub Action. - [ ] Make an announcement in [Pundit discussions](https://github.com/varvet/pundit/discussions/categories/announcements) ================================================ FILE: .github/pull_request_template.md ================================================ ## To do - [ ] I have read the [contributing guidelines](https://github.com/varvet/pundit/contribute). - [ ] I have added relevant tests. - [ ] I have adjusted relevant documentation. - [ ] I have made sure the individual commits are meaningful. - [ ] I have added relevant lines to the CHANGELOG. PS: Thank you for contributing to Pundit ❤️ ================================================ FILE: .github/workflows/main.yml ================================================ name: Main on: push: branches: ["main"] pull_request: workflow_dispatch: permissions: contents: read jobs: matrix-test: runs-on: ubuntu-latest continue-on-error: ${{ matrix.allow-failure || false }} strategy: fail-fast: false matrix: ruby-version: - "3.2" - "3.3" - "3.4" - "4.0" - "jruby-9.4" - "jruby" include: # HEAD-versions - ruby-version: "head" allow-failure: true - ruby-version: "jruby-head" allow-failure: true - ruby-version: "truffleruby-head" allow-failure: true steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: rubygems: latest ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Run tests run: bundle exec rspec test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: rubygems: latest ruby-version: "ruby" bundler-cache: true - name: Run tests run: bundle exec rspec env: COVERAGE: 1 - name: Upload coverage results uses: actions/upload-artifact@v4 with: include-hidden-files: true name: coverage-results path: coverage retention-days: 1 linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: rubygems: default ruby-version: "ruby" bundler-cache: false - run: bundle install - name: Run standard run: bundle exec standardrb docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: rubygems: default ruby-version: "ruby" bundler-cache: false - run: bundle install - run: rake yard required-checks: runs-on: ubuntu-latest if: ${{ always() }} needs: - test - matrix-test - docs - linting steps: - name: failure if: ${{ failure() || contains(needs.*.result, 'failure') }} run: exit 1 - name: success run: exit 0 ================================================ FILE: .github/workflows/push_gem.yml ================================================ name: Push Gem on: workflow_dispatch: permissions: contents: read jobs: push: if: github.repository == 'varvet/pundit' runs-on: ubuntu-latest permissions: contents: write id-token: write steps: # Set up - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Ruby uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 with: bundler-cache: true ruby-version: ruby # Release - uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1+ unreleased ================================================ FILE: .gitignore ================================================ *.gem *.rbc .bundle .config .coverage .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp ================================================ FILE: .rubocop_ignore_git.yml ================================================ # This is here so we can keep YAML syntax highlight in the main file. AllCops: Exclude: - "lib/generators/**/templates/**/*" <% `git status --ignored --porcelain`.lines.grep(/^!! /).each do |path| %> - <%= path.sub(/^!! /, '').sub(/\/$/, '/**/*') %> <% end %> ================================================ FILE: .standard.yml ================================================ parallel: true ruby_version: 3.2 extend_config: - .rubocop_ignore_git.yml ================================================ FILE: .yardopts ================================================ --no-private --private --protected --hide-void-return --markup markdown --fail-on-warning ================================================ FILE: CHANGELOG.md ================================================ # Pundit ## Unreleased - Add support for `params.expect` using `expected_parameters` and `expected_parameters_for`. [#855](https://github.com/varvet/pundit/pull/855) ### Fixed - Update for rspec 4 breaking changes [#873](https://github.com/varvet/pundit/issues/873) ## 2.5.2 (2025-09-24) ### Fixed - Added `config/rubocop-rspec.yml` back from accidentally being excluded [#866](https://github.com/varvet/pundit/issues/866) ## 2.5.1 (2025-09-12) ### Fixed - Requiring only `pundit/rspec` no longer raises an error in Active Support [#857](https://github.com/varvet/pundit/issues/857) ## 2.5.0 (2025-03-03) ### Added - Add `Pundit::Authorization#pundit_reset!` hook to reset the policy and policy scope cache. [#830](https://github.com/varvet/pundit/issues/830) - Add links to gemspec. [#845](https://github.com/varvet/pundit/issues/845) - Register policies directories for Rails 8 code statistics [#833](https://github.com/varvet/pundit/issues/833) - Added an example for how to use pundit with Rails 8 authentication generator [#850](https://github.com/varvet/pundit/issues/850) ### Changed - Deprecated `Pundit::SUFFIX`, moved it to `Pundit::PolicyFinder::SUFFIX` [#835](https://github.com/varvet/pundit/issues/835) - Explicitly require less of `active_support` [#837](https://github.com/varvet/pundit/issues/837) - Using `permit` matcher without a surrouding `permissions` block now raises a useful error. [#836](https://github.com/varvet/pundit/issues/836) ### Fixed - Using a hash as custom cache in `Pundit.authorize` now works as documented. [#838](https://github.com/varvet/pundit/issues/838) ## 2.4.0 (2024-08-26) ### Changed - Improve the `NotAuthorizedError` message to include the policy class. Furthermore, in the case where the record passed is a class instead of an instance, the class name is given. [#812](https://github.com/varvet/pundit/issues/812) ### Added - Add customizable permit matcher description [#806](https://github.com/varvet/pundit/issues/806) - Add support for filter_run_when_matching :focus with permissions helper. [#820](https://github.com/varvet/pundit/issues/820) ## 2.3.2 (2024-05-08) - Refactor: First pass of Pundit::Context [#797](https://github.com/varvet/pundit/issues/797) ### Changed - Update `ApplicationPolicy` generator to qualify the `Scope` class name [#792](https://github.com/varvet/pundit/issues/792) - Policy generator uses `NoMethodError` to indicate `#resolve` is not implemented [#776](https://github.com/varvet/pundit/issues/776) ## Deprecated - Dropped support for Ruby 3.0 [#796](https://github.com/varvet/pundit/issues/796) ## 2.3.1 (2023-07-17) ### Fixed - Use `Kernel.warn` instead of `ActiveSupport::Deprecation.warn` for deprecations [#764](https://github.com/varvet/pundit/issues/764) - Policy generator now works on Ruby 3.2 [#754](https://github.com/varvet/pundit/issues/754) ## 2.3.0 (2022-12-19) ### Added - add support for rubocop-rspec syntax extensions [#745](https://github.com/varvet/pundit/issues/745) ## 2.2.0 (2022-02-11) ### Fixed - Using `policy_class` and a namespaced record now passes only the record when instantiating the policy. (#697, #689, #694, #666) ### Changed - Require users to explicitly define Scope#resolve in generated policies (#711, #722) ### Deprecated - Deprecate `include Pundit` in favor of `include Pundit::Authorization` [#621](https://github.com/varvet/pundit/issues/621) ## 2.1.1 (2021-08-13) Friday 13th-release! Careful! The bugfix below [#626](https://github.com/varvet/pundit/issues/626) could break existing code. If you rely on the return value for `authorize` and namespaced policies you might need to do some changes. ### Fixed - `.authorize` and `#authorize` return the instance, even for namespaced policies [#626](https://github.com/varvet/pundit/issues/626) ### Changed - Generate application scope with `protected` attr_readers. [#616](https://github.com/varvet/pundit/issues/616) ### Removed - Dropped support for Ruby end-of-life versions: 2.1 and 2.2. [#604](https://github.com/varvet/pundit/issues/604) - Dropped support for Ruby end-of-life versions: 2.3 [#633](https://github.com/varvet/pundit/issues/633) - Dropped support for Ruby end-of-life versions: 2.4, 2.5 and JRuby 9.1 [#676](https://github.com/varvet/pundit/issues/676) - Dropped support for RSpec 2 [#615](https://github.com/varvet/pundit/issues/615) ## 2.1.0 (2019-08-14) ### Fixed - Avoid name clashes with the Error class. [#590](https://github.com/varvet/pundit/issues/590) ### Changed - Return a safer default NotAuthorizedError message. [#583](https://github.com/varvet/pundit/issues/583) ## 2.0.1 (2019-01-18) ### Breaking changes None ### Other changes - Improve exception handling for `#policy_scope` and `#policy_scope!`. [#550](https://github.com/varvet/pundit/issues/550) - Add `:policy` metadata to RSpec template. [#566](https://github.com/varvet/pundit/issues/566) ## 2.0.0 (2018-07-21) No changes since beta1 ## 2.0.0.beta1 (2018-07-04) ### Breaking changes - Only pass last element of "namespace array" to policy and scope. [#529](https://github.com/varvet/pundit/issues/529) - Raise `InvalidConstructorError` if a policy or policy scope with an invalid constructor is called. [#462](https://github.com/varvet/pundit/issues/462) - Return passed object from `#authorize` method to make chaining possible. [#385](https://github.com/varvet/pundit/issues/385) ### Other changes - Add `policy_class` option to `authorize` to be able to override the policy. [#441](https://github.com/varvet/pundit/issues/441) - Add `policy_scope_class` option to `authorize` to be able to override the policy scope. [#441](https://github.com/varvet/pundit/issues/441) - Fix `param_key` issue when passed an array. [#529](https://github.com/varvet/pundit/issues/529) - Allow specification of a `NilClassPolicy`. [#525](https://github.com/varvet/pundit/issues/525) - Make sure `policy_class` override is called when passed an array. [#475](https://github.com/varvet/pundit/issues/475) - Use `action_name` instead of `params[:action]`. [#419](https://github.com/varvet/pundit/issues/419) - Add `pundit_params_for` method to make it easy to customize params fetching. [#502](https://github.com/varvet/pundit/issues/502) ## 1.1.0 (2016-01-14) - Can retrieve policies via an array of symbols/objects. - Add autodetection of param key to `permitted_attributes` helper. - Hide some methods which should not be actions. - Permitted attributes should be expanded. - Generator uses `RSpec.describe` according to modern best practices. ## 1.0.1 (2015-05-27) - Fixed a regression where NotAuthorizedError could not be ininitialized with a string. - Use `camelize` instead of `classify` for symbol policies to prevent weird pluralizations. ## 1.0.0 (2015-04-19) - Caches policy scopes and policies. - Explicitly setting the policy for the controller via `controller.policy = foo` has been removed. Instead use `controller.policies[record] = foo`. - Explicitly setting the policy scope for the controller via `controller.policy_policy = foo` has been removed. Instead use `controller.policy_scopes[scope] = foo`. - Add `permitted_attributes` helper to fetch attributes from policy. - Add `pundit_policy_authorized?` and `pundit_policy_scoped?` methods. - Instance variables are prefixed to avoid collisions. - Add `Pundit.authorize` method. - Add `skip_authorization` and `skip_policy_scope` helpers. - Better errors when checking multiple permissions in RSpec tests. - Better errors in case `nil` is passed to `policy` or `policy_scope`. - Use `inspect` when printing object for better errors. - Dropped official support for Ruby 1.9.3 ## 0.3.0 (2014-08-22) - Extend the default `ApplicationPolicy` with an `ApplicationPolicy::Scope` [#120](https://github.com/varvet/pundit/issues/120) - Fix RSpec 3 deprecation warnings for built-in matchers [#162](https://github.com/varvet/pundit/issues/162) - Generate blank policy spec/test files for Rspec/MiniTest/Test::Unit in Rails [#138](https://github.com/varvet/pundit/issues/138) ## 0.2.3 (2014-04-06) - Customizable error messages: `#query`, `#record` and `#policy` methods on `Pundit::NotAuthorizedError` [#114](https://github.com/varvet/pundit/issues/114) - Raise a different `Pundit::AuthorizationNotPerformedError` when `authorize` call is expected in controller action but missing [#109](https://github.com/varvet/pundit/issues/109) - Update Rspec matchers for Rspec 3 [#124](https://github.com/varvet/pundit/issues/124) ## 0.2.2 (2014-02-07) - Customize the user to be passed into policies: `pundit_user` [#42](https://github.com/varvet/pundit/issues/42) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [https://contributor-covenant.org/version/1/0/0/](https://contributor-covenant.org/version/1/0/0/) ================================================ FILE: CONTRIBUTING.md ================================================ ## Security issues If you have found a security related issue, please do not file an issue on GitHub or send a PR addressing the issue. Refer to [SECURITY.md](./SECURITY.md) for instructions. ## Reporting issues Please try to answer the following questions in your bug report: - What did you do? - What did you expect to happen? - What happened instead? Make sure to include as much relevant information as possible. Ruby version, Pundit version, OS version and any stack traces you have are very valuable. ## Pull Requests - **Add tests!** Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour**. Make sure the README and any other relevant documentation are kept up-to-date. - **Create topic branches**. Please don't ask us to pull from your main branch. - **One pull request per feature**. If you want to do more than one thing, send multiple pull requests. - **Send coherent history**. Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before sending them to us. - **Update the CHANGELOG.** Don't forget to add your new changes to the CHANGELOG. ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gemspec # Rails-related - for testing purposes gem "actionpack", ">= 3.0.0" # Used to test strong parameters gem "activemodel", ">= 3.0.0" # Used to test ActiveModel::Naming gem "railties", ">= 3.0.0" # Used to test generators # Testing gem "rspec", ">= 3.0.0" gem "simplecov", ">= 0.17.0" # Development tools gem "bundler" gem "rake" gem "standard" gem "yard" gem "zeitwerk" # Affects us on JRuby 9.3.15. # # @see https://github.com/rails/rails/issues/54260 gem "logger" ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2019 Jonas Nicklas, Varvet AB MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Pundit [![Main](https://github.com/varvet/pundit/actions/workflows/main.yml/badge.svg)](https://github.com/varvet/pundit/actions/workflows/main.yml) [![Inline docs](https://inch-ci.org/github/varvet/pundit.svg?branch=main)](https://inch-ci.org/github/varvet/pundit) [![Gem Version](https://badge.fury.io/rb/pundit.svg)](https://badge.fury.io/rb/pundit) Pundit provides a set of helpers which guide you in leveraging regular Ruby classes and object oriented design patterns to build a straightforward, robust, and scalable authorization system. ## Links: - [API documentation for the most recent version](https://www.rubydoc.info/gems/pundit) - [Source Code](https://github.com/varvet/pundit) - [Contributing](https://github.com/varvet/pundit/blob/main/CONTRIBUTING.md) - [Code of Conduct](https://github.com/varvet/pundit/blob/main/CODE_OF_CONDUCT.md) Sponsored by: Varvet

Varvet logo ## Installation > **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). ``` sh bundle add pundit ``` Include `Pundit::Authorization` in your application controller: ``` ruby class ApplicationController < ActionController::Base include Pundit::Authorization end ``` Optionally, you can run the generator, which will set up an application policy with some useful defaults for you: ``` sh rails g pundit:install ``` After generating your application policy, restart the Rails server so that Rails can pick up any classes in the new `app/policies/` directory. ## Policies Pundit is focused around the notion of policy classes. We suggest that you put these classes in `app/policies`. This is an example that allows updating a post if the user is an admin, or if the post is unpublished: ``` ruby class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? || !post.published? end end ``` As you can see, this is a plain Ruby class. Pundit makes the following assumptions about this class: - The class has the same name as some kind of model class, only suffixed with the word "Policy". - The first argument is a user. In your controller, Pundit will call the `current_user` method to retrieve what to send into this argument - The second argument is some kind of model object, whose authorization you want to check. This does not need to be an ActiveRecord or even an ActiveModel object, it can be anything really. - The class implements some kind of query method, in this case `update?`. Usually, this will map to the name of a particular controller action. That's it really. Usually you'll want to inherit from the application policy created by the generator, or set up your own base class to inherit from: ``` ruby class PostPolicy < ApplicationPolicy def update? user.admin? or not record.published? end end ``` In the generated `ApplicationPolicy`, the model object is called `record`. Supposing that you have an instance of class `Post`, Pundit now lets you do this in your controller: ``` ruby def update @post = Post.find(params[:id]) authorize @post if @post.update(post_params) redirect_to @post else render :edit end end ``` The authorize method automatically infers that `Post` will have a matching `PostPolicy` class, and instantiates this class, handing in the current user and the given record. It then infers from the action name, that it should call `update?` on this instance of the policy. In this case, you can imagine that `authorize` would have done something like this: ``` ruby unless PostPolicy.new(current_user, @post).update? raise Pundit::NotAuthorizedError, "not allowed to PostPolicy#update? this Post" end ``` You can pass a second argument to `authorize` if the name of the permission you want to check doesn't match the action name. For example: ``` ruby def publish @post = Post.find(params[:id]) authorize @post, :update? @post.publish! redirect_to @post end ``` You can pass an argument to override the policy class if necessary. For example: ```ruby def create @publication = find_publication # assume this method returns any model that behaves like a publication # @publication.class => Post authorize @publication, policy_class: PublicationPolicy @publication.publish! redirect_to @publication end ``` If you don't have an instance for the first argument to `authorize`, then you can pass the class. For example: Policy: ```ruby class PostPolicy < ApplicationPolicy def admin_list? user.admin? end end ``` Controller: ```ruby def admin_list authorize Post # we don't have a particular post to authorize # Rest of controller action end ``` `authorize` returns the instance passed to it, so you can chain it like this: Controller: ```ruby def show @user = authorize User.find(params[:id]) end ``` You can easily get a hold of an instance of the policy through the `policy` method in both the view and controller. This is especially useful for conditionally showing links or buttons in the view: ``` erb <% if policy(@post).update? %> <%= link_to "Edit post", edit_post_path(@post) %> <% end %> ``` ## Headless policies Given there is a policy without a corresponding model / ruby class, you can retrieve it by passing a symbol. ```ruby # app/policies/dashboard_policy.rb class DashboardPolicy attr_reader :user # `_record` in this example will be :dashboard def initialize(user, _record) @user = user end def show? user.admin? end end ``` Note that the headless policy still needs to accept two arguments. The second argument will be the symbol `:dashboard` in this case, which is what is passed as the record to `authorize` below. ```ruby # In controllers def show authorize :dashboard, :show? ... end ``` ```erb # In views <% if policy(:dashboard).show? %> <%= link_to 'Dashboard', dashboard_path %> <% end %> ``` ## Scopes Often, you will want to have some kind of view listing records which a particular user has access to. When using Pundit, you are expected to define a class called a policy scope. It can look something like this: ``` ruby class PostPolicy < ApplicationPolicy class Scope def initialize(user, scope) @user = user @scope = scope end def resolve if user.admin? scope.all else scope.where(published: true) end end private attr_reader :user, :scope end def update? user.admin? or not record.published? end end ``` Pundit makes the following assumptions about this class: - The class has the name `Scope` and is nested under the policy class. - The first argument is a user. In your controller, Pundit will call the `current_user` method to retrieve what to send into this argument. - The second argument is a scope of some kind on which to perform some kind of query. It will usually be an ActiveRecord class or a `ActiveRecord::Relation`, but it could be something else entirely. - Instances of this class respond to the method `resolve`, which should return some kind of result which can be iterated over. For ActiveRecord classes, this would usually be an `ActiveRecord::Relation`. You'll probably want to inherit from the application policy scope generated by the generator, or create your own base class to inherit from: ``` ruby class PostPolicy < ApplicationPolicy class Scope < ApplicationPolicy::Scope def resolve if user.admin? scope.all else scope.where(published: true) end end end def update? user.admin? or not record.published? end end ``` You can now use this class from your controller via the `policy_scope` method: ``` ruby def index @posts = policy_scope(Post) end def show @post = policy_scope(Post).find(params[:id]) end ``` Like with the authorize method, you can also override the policy scope class: ``` ruby def index # publication_class => Post @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope) end ``` In this case it is a shortcut for doing: ``` ruby def index @publications = PublicationPolicy::Scope.new(current_user, Post).resolve end ``` You can, and are encouraged to, use this method in views: ``` erb <% policy_scope(@user.posts).each do |post| %>

<%= link_to post.title, post_path(post) %>

<% end %> ``` ## Ensuring policies and scopes are used When you are developing an application with Pundit it can be easy to forget to authorize some action. People are forgetful after all. Since Pundit encourages you to add the `authorize` call manually to each controller action, it's really easy to miss one. Thankfully, Pundit has a handy feature which reminds you in case you forget. Pundit tracks whether you have called `authorize` anywhere in your controller action. Pundit also adds a method to your controllers called `verify_authorized`. This method will raise an exception if `authorize` has not yet been called. You should run this method in an `after_action` hook to ensure that you haven't forgotten to authorize the action. For example: ``` ruby class ApplicationController < ActionController::Base include Pundit::Authorization after_action :verify_authorized end ``` Likewise, Pundit also adds `verify_policy_scoped` to your controller. This will raise an exception similar to `verify_authorized`. However, it tracks if `policy_scope` is used instead of `authorize`. This is mostly useful for controller actions like `index` which find collections with a scope and don't authorize individual instances. ``` ruby class ApplicationController < ActionController::Base include Pundit::Authorization after_action :verify_pundit_authorization def verify_pundit_authorization if action_name == "index" verify_policy_scoped else verify_authorized end end end ``` **This verification mechanism only exists to aid you while developing your application, so you don't forget to call `authorize`. It is not some kind of failsafe mechanism or authorization mechanism. You should be able to remove these filters without affecting how your app works in any way.** Some people have found this feature confusing, while many others find it extremely helpful. If you fall into the category of people who find it confusing then you do not need to use it. Pundit will work fine without using `verify_authorized` and `verify_policy_scoped`. ### Conditional verification If you're using `verify_authorized` in your controllers but need to conditionally bypass verification, you can use `skip_authorization`. For bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful in circumstances where you don't want to disable verification for the entire action, but have some cases where you intend to not authorize. ```ruby def show record = Record.find_by(attribute: "value") if record.present? authorize record else skip_authorization end end ``` ## Manually specifying policy classes Sometimes you might want to explicitly declare which policy to use for a given class, instead of letting Pundit infer it. This can be done like so: ``` ruby class Post def self.policy_class PostablePolicy end end ``` Alternatively, you can declare an instance method: ``` ruby class Post def policy_class PostablePolicy end end ``` ## Plain old Ruby 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. 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. 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. ## Generator Use the supplied generator to generate policies: ``` sh rails g pundit:policy post ``` ## Closed systems In many applications, only logged in users are really able to do anything. If you're building such a system, it can be kind of cumbersome to check that the user in a policy isn't `nil` for every single permission. Aside from policies, you can add this check to the base class for scopes. We suggest that you define a filter that redirects unauthenticated users to the login page. As a secondary defence, if you've defined an ApplicationPolicy, it might be a good idea to raise an exception if somehow an unauthenticated user got through. This way you can fail more gracefully. ``` ruby class ApplicationPolicy def initialize(user, record) raise Pundit::NotAuthorizedError, "must be logged in" unless user @user = user @record = record end class Scope attr_reader :user, :scope def initialize(user, scope) raise Pundit::NotAuthorizedError, "must be logged in" unless user @user = user @scope = scope end end end ``` ## NilClassPolicy To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) you may find that you want to implement a `NilClassPolicy`. This might be useful where you want to extend your ApplicationPolicy to allow some tolerance of, for example, associations which might be `nil`. ```ruby class NilClassPolicy < ApplicationPolicy class Scope < ApplicationPolicy::Scope def resolve raise Pundit::NotDefinedError, "Cannot scope NilClass" end end def show? false # Nobody can see nothing end end ``` ## Rescuing a denied Authorization in Rails Pundit raises a `Pundit::NotAuthorizedError` you can [rescue_from](https://guides.rubyonrails.org/action_controller_overview.html#rescue-from) in your `ApplicationController`. You can customize the `user_not_authorized` method in every controller. ```ruby class ApplicationController < ActionController::Base include Pundit::Authorization rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_back_or_to(root_path) end end ``` 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: ```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden``` ## Creating custom error messages `NotAuthorizedError`s provide information on what query (e.g. `:create?`), what record (e.g. an instance of `Post`), and what policy (e.g. an instance of `PostPolicy`) caused the error to be raised. One way to use these `query`, `record`, and `policy` properties is to connect them with `I18n` to generate error messages. Here's how you might go about doing that. ```ruby class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized(exception) policy_name = exception.policy.class.to_s.underscore flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default redirect_back_or_to(root_path) end end ``` ```yaml en: pundit: default: 'You cannot perform this action.' post_policy: update?: 'You cannot edit this post!' create?: 'You cannot create posts!' ``` This is an example. Pundit is agnostic as to how you implement your error messaging. ## Manually retrieving policies and scopes Sometimes you want to retrieve a policy for a record outside the controller or view. For example when you delegate permissions from one policy to another. You can easily retrieve policies and scopes like this: ``` ruby Pundit.policy!(user, post) Pundit.policy(user, post) Pundit.policy_scope!(user, Post) Pundit.policy_scope(user, Post) ``` The bang methods will raise an exception if the policy does not exist, whereas those without the bang will return nil. ## Customize Pundit user 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`. ```ruby def pundit_user User.find_by_other_means end ``` 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`. To ensure compatibility with Pundit, define a `pundit_user` method in `application_controller.rb` (or another suitable location) as follows: ```ruby def pundit_user Current.user end ``` ### Handling User Switching in Pundit 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. To handle user switching, you can use the following pattern in your controller: ```ruby class ApplicationController include Pundit::Authorization def switch_user_to(user) terminate_session if authenticated? start_new_session_for user pundit_reset! end end ``` 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. ## Policy Namespacing In some cases it might be helpful to have multiple policies that serve different contexts for a resource. A prime example of this is the case where User policies differ from Admin policies. To authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array: ```ruby authorize(post) # => will look for a PostPolicy authorize([:admin, post]) # => will look for an Admin::PostPolicy authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy policy_scope(Post) # => will look for a PostPolicy::Scope policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope ``` If you are using namespaced policies for something like Admin areas, we recommend defining a `pundit_namespace` hook in your `ApplicationController` and overriding it in namespaced controllers: ```ruby class ApplicationController < ActionController::Base include Pundit::Authorization private def pundit_namespace(record) = record def authorize(record, ...) = super(pundit_namespace(record), ...) def policy_scope(scope, ...) = super(pundit_namespace(scope), ...) end class AdminController < ApplicationController private # Override the pundit namespace in Admin. def pundit_namespace(record) = [:admin, record] end class Admin::PostController < AdminController def index policy_scope(Post) end def show post = authorize Post.find(params[:id]) end end ``` ## Additional context Pundit strongly encourages you to model your application in such a way that the only context you need for authorization is a user object and a domain model that you want to check authorization for. If you find yourself needing more context than that, consider whether you are authorizing the right domain model, maybe another domain model (or a wrapper around multiple domain models) can provide the context you need. Pundit does not allow you to pass additional arguments to policies for precisely this reason. However, in very rare cases, you might need to authorize based on more context than just the currently authenticated user. Suppose for example that authorization is dependent on IP address in addition to the authenticated user. In that case, one option is to create a special class which wraps up both user and IP and passes it to the policy. ``` ruby class UserContext < Data.define(:user, :ip) end class ApplicationController include Pundit::Authorization def pundit_user = UserContext.new(current_user, request.ip) end ``` ## Strong parameters In Rails, [mass-assignment protection is handled in the controller](https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters). With Pundit you can control which attributes a user has access to update via your policies. You can set up an `expected_attributes_for_action(action_name)` method in your policy like this: ```ruby # app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def expected_attributes_for_action(_action_name) if user.admin? || user.owner_of?(post) [:title, :body, :tag_list] else [:tag_list] end end end ``` You can now retrieve these attributes from the policy: ```ruby # app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update(post_params) redirect_to @post else render :edit end end private def post_params params.expect(policy(@post).expected_attributes_for_action(action_name)) end end ``` However, this is a bit cumbersome, so Pundit provides a convenient helper method with `#expected_attributes`: ```ruby # app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) if @post.update(expected_attributes(@post)) redirect_to @post else render :edit end end end ``` ### Permitted Parameters Pundit still support the old `params.require.permit()` style of permitting attributes, although `params.expect()` is preferred. 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`. ```ruby def pundit_params_for(record) params.require(pundit_param_key(record)) end ``` For example: ```ruby # If you don't want to use require def pundit_params_for(record) params.fetch(pundit_param_key(record), {}) end # If you are using something like the JSON API spec def pundit_params_for(_record) params.fetch(:data, {}).fetch(:attributes, {}) end ``` ## RSpec ### Policy Specs > [!TIP] > An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this [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. Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec. Require `pundit/rspec` in your `spec_helper.rb`: ``` ruby require "pundit/rspec" ``` Then put your policy specs in `spec/policies`, and make them look somewhat like this: ``` ruby describe PostPolicy do subject { described_class } permissions :update?, :edit? do it "denies access if post is published" do expect(subject).not_to permit(User.new(admin: false), Post.new(published: true)) end it "grants access if post is published and user is an admin" do expect(subject).to permit(User.new(admin: true), Post.new(published: true)) end it "grants access if post is unpublished" do expect(subject).to permit(User.new(admin: false), Post.new(published: false)) end end end ``` ### Custom matcher description By default rspec includes an inspected `user` and `record` in the matcher description, which might become overly verbose: ``` PostPolicy update? and show? is expected to permit # and #> ``` You can override the default description with a static string, or a block: ```ruby # static alternative: Pundit::RSpec::Matchers.description = "permit the user" Pundit::RSpec::Matchers.description = ->(user, record) do "permit user with role #{user.role} to access record with ID #{record.id}" end ``` Which would make for a less chatty output: ``` PostPolicy update? and show? is expected to permit user with role admin to access record with ID 130 ``` ### Focus Support If your RSpec config has `filter_run_when_matching :focus`, you may tag the `permissions` helper like so: ``` permissions :show?, :focus do ``` ### Scope Specs Pundit does not provide a DSL for testing scopes. Test them like you would a regular Ruby class! ### Linting with RuboCop RSpec When you lint your RSpec spec files with `rubocop-rspec`, it will fail to properly detect RSpec constructs that Pundit defines, `permissions`. Make sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`: ```yaml inherit_gem: pundit: config/rubocop-rspec.yml ``` # External Resources - [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit) - [Migrating to Pundit from CanCan](https://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/) - [Testing Pundit Policies with RSpec](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) - [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450) - [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136) - [Straightforward Rails Authorization with Pundit](https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/) ## Other implementations - [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](https://flask.pocoo.org/) extension "heavily inspired by" Pundit # License Licensed under the MIT license, see the separate LICENSE.txt file. ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require "rubygems" require "bundler/gem_tasks" require "rspec/core/rake_task" require "yard" require "rubocop/rake_task" RuboCop::RakeTask.new desc "Run all examples" RSpec::Core::RakeTask.new(:spec) YARD::Rake::YardocTask.new do |t| t.files = ["lib/**/*.rb"] t.stats_options = ["--list-undoc"] end task default: :spec ================================================ FILE: SECURITY.md ================================================ # Security Policy Please do not file an issue on GitHub, or send a PR addressing the issue. ## Supported versions Most recent major version only. ## Reporting a vulnerability Contact one of the maintainers directly: * [@Burgestrand](https://github.com/Burgestrand) * [@dgmstuart](https://github.com/dgmstuart) * [@varvet](https://github.com/varvet) You can report vulnerabilities on GitHub too: https://github.com/varvet/pundit/security Thank you! ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "pundit" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. require "irb" IRB.start(__FILE__) ================================================ FILE: bin/setup ================================================ #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here ================================================ FILE: config/rubocop-rspec.yml ================================================ RSpec: Language: ExampleGroups: Regular: - permissions ================================================ FILE: lib/generators/pundit/install/USAGE ================================================ Description: Generates an application policy as a starting point for your application. ================================================ FILE: lib/generators/pundit/install/install_generator.rb ================================================ # frozen_string_literal: true module Pundit # @private module Generators # @private class InstallGenerator < ::Rails::Generators::Base source_root File.expand_path("templates", __dir__) def copy_application_policy template "application_policy.rb.tt", "app/policies/application_policy.rb" end end end end ================================================ FILE: lib/generators/pundit/install/templates/application_policy.rb.tt ================================================ # frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end class Scope def initialize(user, scope) @user = user @scope = scope end def resolve raise NoMethodError, "You must define #resolve in #{self.class}" end private attr_reader :user, :scope end end ================================================ FILE: lib/generators/pundit/policy/USAGE ================================================ Description: Generates a policy for a model with the given name. Example: rails generate pundit:policy user This will create: app/policies/user_policy.rb ================================================ FILE: lib/generators/pundit/policy/policy_generator.rb ================================================ # frozen_string_literal: true module Pundit # @private module Generators # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy template "policy.rb.tt", File.join("app/policies", class_path, "#{file_name}_policy.rb") end hook_for :test_framework end end end ================================================ FILE: lib/generators/pundit/policy/templates/policy.rb.tt ================================================ <% module_namespacing do -%> class <%= class_name %>Policy < ApplicationPolicy # NOTE: Up to Pundit v2.3.1, the inheritance was declared as # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`. # In most cases the behavior will be identical, but if updating existing # code, beware of possible changes to the ancestors: # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5 class Scope < ApplicationPolicy::Scope # NOTE: Be explicit about which records you allow access to! # def resolve # scope.all # end end end <% end -%> ================================================ FILE: lib/generators/rspec/policy_generator.rb ================================================ # frozen_string_literal: true # @private module Rspec # @private module Generators # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy_spec template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb") end end end end ================================================ FILE: lib/generators/rspec/templates/policy_spec.rb.tt ================================================ require '<%= File.exist?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>' RSpec.describe <%= class_name %>Policy, type: :policy do let(:user) { User.new } subject { described_class } permissions ".scope" do pending "add some examples to (or delete) #{__FILE__}" end permissions :show? do pending "add some examples to (or delete) #{__FILE__}" end permissions :create? do pending "add some examples to (or delete) #{__FILE__}" end permissions :update? do pending "add some examples to (or delete) #{__FILE__}" end permissions :destroy? do pending "add some examples to (or delete) #{__FILE__}" end end ================================================ FILE: lib/generators/test_unit/policy_generator.rb ================================================ # frozen_string_literal: true # @private module TestUnit # @private module Generators # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy_test template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb") end end end end ================================================ FILE: lib/generators/test_unit/templates/policy_test.rb.tt ================================================ require 'test_helper' class <%= class_name %>PolicyTest < ActiveSupport::TestCase def test_scope end def test_show end def test_create end def test_update end def test_destroy end end ================================================ FILE: lib/pundit/authorization.rb ================================================ # frozen_string_literal: true module Pundit # Pundit DSL to include in your controllers to provide authorization helpers. # # @example # class ApplicationController < ActionController::Base # include Pundit::Authorization # end # @see #pundit # @api public # @since v2.2.0 module Authorization extend ActiveSupport::Concern included do helper Helper if respond_to?(:helper) if respond_to?(:helper_method) helper_method :policy helper_method :pundit_policy_scope helper_method :pundit_user end end protected # An instance of {Pundit::Context} initialized with the current user. # # @note this method is memoized and will return the same instance during the request. # @api public # @return [Pundit::Context] # @see #pundit_user # @see #policies # @since v2.3.2 def pundit @pundit ||= Pundit::Context.new( user: pundit_user, policy_cache: Pundit::CacheStore::LegacyStore.new(policies) ) end # Hook method which allows customizing which user is passed to policies and # scopes initialized by {#authorize}, {#policy} and {#policy_scope}. # # @note Make sure to call `pundit_reset!` if this changes during a request. # @see https://github.com/varvet/pundit#customize-pundit-user # @see #pundit # @see #pundit_reset! # @return [Object] the user object to be used with pundit # @since v0.2.2 def pundit_user current_user end # Clears the cached Pundit authorization data. # # This method should be called when the pundit_user is changed, # such as during user switching, to ensure that stale authorization # data is not used. Pundit caches authorization policies and scopes # for the pundit_user, so calling this method will reset those # caches and ensure that the next authorization checks are performed # with the correct context for the new pundit_user. # # @return [void] # @since v2.5.0 def pundit_reset! @pundit = nil @_pundit_policies = nil @_pundit_policy_scopes = nil @_pundit_policy_authorized = nil @_pundit_policy_scoped = nil end # @!group Policies # Retrieves the policy for the given record, initializing it with the record # and current user and finally throwing an error if the user is not # authorized to perform the given action. # # @param record [Object, Array] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`). # If omitted then this defaults to the Rails controller action name. # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [record] Always returns the passed object record # @see Pundit::Context#authorize # @see #verify_authorized # @since v0.1.0 def authorize(record, query = nil, policy_class: nil) query ||= "#{action_name}?" @_pundit_policy_authorized = true pundit.authorize(record, query: query, policy_class: policy_class) end # Allow this action not to perform authorization. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @return [void] # @see #verify_authorized # @since v1.0.0 def skip_authorization @_pundit_policy_authorized = :skipped end # @return [Boolean] wether or not authorization has been performed # @see #authorize # @see #skip_authorization # @since v1.0.0 def pundit_policy_authorized? !!@_pundit_policy_authorized end # Raises an error if authorization has not been performed. # # Usually used as an `after_action` filter to prevent programmer error in # forgetting to call {#authorize} or {#skip_authorization}. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @raise [AuthorizationNotPerformedError] if authorization has not been performed # @return [void] # @see #authorize # @see #skip_authorization # @since v0.1.0 def verify_authorized raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized? end # rubocop:disable Naming/MemoizedInstanceVariableName # Cache of policies. You should not rely on this method. # # @api private # @since v1.0.0 def policies @_pundit_policies ||= {} end # rubocop:enable Naming/MemoizedInstanceVariableName # @!endgroup # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param record [Object] the object we're retrieving the policy for # @return [Object] instance of policy class with query methods # @since v0.1.0 def policy(record) pundit.policy!(record) end # @!group Policy Scopes # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes # @param scope [Object] the object we're retrieving the policy scope for # @param policy_scope_class [#resolve] the policy scope class we want to force use of # @return [#resolve, nil] instance of scope class which can resolve to a scope # @since v0.1.0 def policy_scope(scope, policy_scope_class: nil) @_pundit_policy_scoped = true policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope) end # Allow this action not to perform policy scoping. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @return [void] # @see #verify_policy_scoped # @since v1.0.0 def skip_policy_scope @_pundit_policy_scoped = :skipped end # @return [Boolean] wether or not policy scoping has been performed # @see #policy_scope # @see #skip_policy_scope # @since v1.0.0 def pundit_policy_scoped? !!@_pundit_policy_scoped end # Raises an error if policy scoping has not been performed. # # Usually used as an `after_action` filter to prevent programmer error in # forgetting to call {#policy_scope} or {#skip_policy_scope} in index # actions. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed # @return [void] # @see #policy_scope # @see #skip_policy_scope # @since v0.2.1 def verify_policy_scoped raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped? end # rubocop:disable Naming/MemoizedInstanceVariableName # Cache of policy scope. You should not rely on this method. # # @api private # @since v1.0.0 def policy_scopes @_pundit_policy_scopes ||= {} end # rubocop:enable Naming/MemoizedInstanceVariableName # This was added to allow calling `policy_scope!` without flipping the # `pundit_policy_scoped?` flag. # # It's used internally by `policy_scope`, as well as from the views # when they call `policy_scope`. It works because views get their helper # from {Pundit::Helper}. # # @note This also memoizes the instance with `scope` as the key. # @see Pundit::Helper#policy_scope # @api private # @since v1.0.0 def pundit_policy_scope(scope) policy_scopes[scope] ||= pundit.policy_scope!(scope) end private :pundit_policy_scope # @!endgroup # @!group Strong Parameters # Retrieves a set of expected attributes from the policy. # # @example # if @post.update(expected_attributes(@post)) # redirect_to @post # else # render :edit # end # # @see https://github.com/varvet/pundit#strong-parameters # @see https://guides.rubyonrails.org/action_controller_overview.html#expect # @param record [Object] the object we're retrieving expected attributes for # @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`). # If omitted then this defaults to the Rails controller action name. # @param param_key [String] the key that the record would have in the params hash # @return [Hash{String => Object}] the expected attributes # @since v2.6.0 def expected_attributes(record, action: action_name, param_key: pundit_param_key(record)) policy = policy(record) params.expect(param_key => policy.expected_attributes_for_action(action)) end # @note This is provided as a hook for overrides. # @param record [Object] # @return [String] the key that the record would have in the params hash # @since v2.6.0 def pundit_param_key(record) PolicyFinder.new(record).param_key end # Retrieves a set of permitted attributes from the policy. # # Done by instantiating the policy class for the given record and calling # `permitted_attributes` on it, or `permitted_attributes_for_{action}` if # `action` is defined. It then infers what key the record should have in the # params hash and retrieves the permitted attributes from the params hash # under that key. # # @see https://github.com/varvet/pundit#strong-parameters # @param record [Object] the object we're retrieving permitted attributes for # @param action [Symbol, String] the name of the action being performed on the record (e.g. `update`). # If omitted then this defaults to the Rails controller action name. # @return [Hash{String => Object}] the permitted attributes # @since v1.0.0 def permitted_attributes(record, action = action_name) policy = policy(record) method_name = if policy.respond_to?("permitted_attributes_for_#{action}") "permitted_attributes_for_#{action}" else "permitted_attributes" end pundit_params_for(record).permit(*policy.public_send(method_name)) end # Retrieves the params for the given record. # # @param record [Object] the object we're retrieving params for # @return [ActionController::Parameters] the params # @since v2.0.0 def pundit_params_for(record) params.require(pundit_param_key(record)) end # @!endgroup end end ================================================ FILE: lib/pundit/cache_store/legacy_store.rb ================================================ # frozen_string_literal: true module Pundit module CacheStore # A cache store that uses only the record as a cache key, and ignores the user. # # The original cache mechanism used by Pundit. # # @api private # @since v2.3.2 class LegacyStore # @since v2.3.2 def initialize(hash = {}) @store = hash end # A cache store that uses only the record as a cache key, and ignores the user. # # @note `nil` results are not cached. # @since v2.3.2 def fetch(user:, record:) _ = user @store[record] ||= yield end end end end ================================================ FILE: lib/pundit/cache_store/null_store.rb ================================================ # frozen_string_literal: true module Pundit module CacheStore # A cache store that does not cache anything. # # Use `NullStore.instance` to get the singleton instance, it is thread-safe. # # @see Pundit::Context#initialize # @api private # @since v2.3.2 class NullStore @instance = new class << self # @since v2.3.2 # @return [NullStore] the singleton instance attr_reader :instance end # Always yields, does not cache anything. # @yield # @return [any] whatever the block returns. # @since v2.3.2 def fetch(*, **) yield end end end end ================================================ FILE: lib/pundit/cache_store.rb ================================================ # frozen_string_literal: true module Pundit # Namespace for cache store implementations. # # Cache stores are used to cache policy lookups, so you get the same policy # instance for the same record. # @since v2.3.2 module CacheStore # @!group Cache Store Interface # @!method fetch(user:, record:, &block) # Looks up a stored policy or generate a new one. # # @since v2.3.2 # @note This is a method template, but the method does not exist in this module. # @param user [Object] the user that initiated the action # @param record [Object] the object being accessed # @param block [Proc] the block to execute if missing # @return [Object] the policy # @!endgroup end end ================================================ FILE: lib/pundit/context.rb ================================================ # frozen_string_literal: true module Pundit # {Pundit::Context} is intended to be created once per request and user, and # it is then used to perform authorization checks throughout the request. # # @example Using Sinatra # helpers do # def current_user = ... # # def pundit # @pundit ||= Pundit::Context.new(user: current_user) # end # end # # get "/posts/:id" do |id| # pundit.authorize(Post.find(id), query: :show?) # end # # @example Using [Roda](https://roda.jeremyevans.net/index.html) # route do |r| # context = Pundit::Context.new(user:) # # r.get "posts", Integer do |id| # context.authorize(Post.find(id), query: :show?) # end # end # # @since v2.3.2 class Context # @see Pundit::Authorization#pundit # @param user later passed to policies and scopes # @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore}) # @since v2.3.2 def initialize(user:, policy_cache: CacheStore::NullStore.instance) @user = user @policy_cache = policy_cache end # @api public # @see #initialize # @since v2.3.2 attr_reader :user # @api private # @see #initialize # @since v2.3.2 attr_reader :policy_cache # @!group Policies # Retrieves the policy for the given record, initializing it with the # record and user and finally throwing an error if the user is not # authorized to perform the given action. # # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`) # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [Object] Always returns the passed object record # @since v2.3.2 def authorize(possibly_namespaced_record, query:, policy_class:) record = pundit_model(possibly_namespaced_record) policy = if policy_class policy_class.new(user, record) else policy!(possibly_namespaced_record) end raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query) record end # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param record [Object] the object we're retrieving the policy for # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Object, nil] instance of policy class with query methods # @since v2.3.2 def policy(record) cached_find(record, &:policy) end # Retrieves the policy for the given record, or raises if not found. # # @see https://github.com/varvet/pundit#policies # @param record [Object] the object we're retrieving the policy for # @raise [NotDefinedError] if the policy cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Object] instance of policy class with query methods # @since v2.3.2 def policy!(record) cached_find(record, &:policy!) end # @!endgroup # @!group Scopes # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes # @param scope [Object] the object we're retrieving the policy scope for # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope # @since v2.3.2 def policy_scope(scope) policy_scope_class = policy_finder(scope).scope return unless policy_scope_class begin policy_scope = policy_scope_class.new(user, pundit_model(scope)) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called" end policy_scope.resolve end # Retrieves the policy scope for the given record. Raises if not found. # # @see https://github.com/varvet/pundit#scopes # @param scope [Object] the object we're retrieving the policy scope for # @raise [NotDefinedError] if the policy scope cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}] instance of scope class which can resolve to a scope # @since v2.3.2 def policy_scope!(scope) policy_scope_class = policy_finder(scope).scope! begin policy_scope = policy_scope_class.new(user, pundit_model(scope)) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called" end policy_scope.resolve end # @!endgroup private # @!group Private Helpers # Finds a cached policy for the given record, or yields to find one. # # @api private # @param record [Object] the object we're retrieving the policy for # @yield a policy finder if no policy was cached # @yieldparam [PolicyFinder] policy_finder # @yieldreturn [#new(user, model)] # @return [Policy, nil] an instantiated policy # @raise [InvalidConstructorError] if policy can't be instantated # @since v2.3.2 def cached_find(record) policy_cache.fetch(user: user, record: record) do klass = yield policy_finder(record) next unless klass model = pundit_model(record) begin klass.new(user, model) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called" end end end # Return a policy finder for the given record. # # @api private # @return [PolicyFinder] # @since v2.3.2 def policy_finder(record) PolicyFinder.new(record) end # Given a possibly namespaced record, return the actual record. # # @api private # @since v2.3.2 def pundit_model(record) record.is_a?(Array) ? record.last : record end end end ================================================ FILE: lib/pundit/error.rb ================================================ # frozen_string_literal: true module Pundit # @api private # @since v1.0.0 # To avoid name clashes with common Error naming when mixing in Pundit, # keep it here with compact class style definition. class Error < StandardError; end # Error that will be raised when authorization has failed # @since v0.1.0 class NotAuthorizedError < Error # @see #initialize # @since v0.2.3 attr_reader :query # @see #initialize # @since v0.2.3 attr_reader :record # @see #initialize # @since v0.2.3 attr_reader :policy # @since v1.0.0 # # @overload initialize(message) # Create an error with a simple error message. # @param [String] message A simple error message string. # # @overload initialize(options) # Create an error with the specified attributes. # @param [Hash] options The error options. # @option options [String] :message Optional custom error message. Will default to a generalized message. # @option options [Symbol] :query The name of the policy method that was checked. # @option options [Object] :record The object that was being checked with the policy. # @option options [Class] :policy The class of policy that was used for the check. def initialize(options = {}) if options.is_a? String message = options else @query = options[:query] @record = options[:record] @policy = options[:policy] message = options.fetch(:message) do record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}" "not allowed to #{policy.class}##{query} #{record_name}" end end super(message) end end # Error that will be raised if a policy or policy scope constructor is not called correctly. # @since v2.0.0 class InvalidConstructorError < Error; end # Error that will be raised if a controller action has not called the # `authorize` or `skip_authorization` methods. # @since v0.2.3 class AuthorizationNotPerformedError < Error; end # Error that will be raised if a controller action has not called the # `policy_scope` or `skip_policy_scope` methods. # @since v0.3.0 class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end # Error that will be raised if a policy or policy scope is not defined. # @since v0.1.0 class NotDefinedError < Error; end end ================================================ FILE: lib/pundit/helper.rb ================================================ # frozen_string_literal: true module Pundit # Rails view helpers, to allow a slightly different view-specific # implementation of the methods in {Pundit::Authorization}. # # @api private # @since v1.0.0 module Helper # @see Pundit::Authorization#pundit_policy_scope # @since v1.0.0 def policy_scope(scope) pundit_policy_scope(scope) end end end ================================================ FILE: lib/pundit/policy_finder.rb ================================================ # frozen_string_literal: true # String#safe_constantize, String#demodulize, String#underscore, String#camelize require "active_support/core_ext/string/inflections" module Pundit # Finds policy and scope classes for given object. # @since v0.1.0 # @api public # @example # user = User.find(params[:id]) # finder = PolicyFinder.new(user) # finder.policy #=> UserPolicy # finder.scope #=> UserPolicy::Scope # class PolicyFinder # A constant applied to the end of the class name to find the policy class. # # @api private # @since v2.5.0 SUFFIX = "Policy" # @see #initialize # @since v0.1.0 attr_reader :object # @param object [any] the object to find policy and scope classes for # @since v0.1.0 def initialize(object) @object = object end # @return [nil, Scope{#resolve}] scope class which can resolve to a scope # @see https://github.com/varvet/pundit#scopes # @example # scope = finder.scope #=> UserPolicy::Scope # scope.resolve #=> <#ActiveRecord::Relation ...> # # @since v0.1.0 def scope "#{policy}::Scope".safe_constantize end # @return [nil, Class] policy class with query methods # @see https://github.com/varvet/pundit#policies # @example # policy = finder.policy #=> UserPolicy # policy.show? #=> true # policy.update? #=> false # # @since v0.1.0 def policy klass = find(object) klass.is_a?(String) ? klass.safe_constantize : klass end # @return [Scope{#resolve}] scope class which can resolve to a scope # @raise [NotDefinedError] if scope could not be determined # # @since v0.1.0 def scope! scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`" end # @return [Class] policy class with query methods # @raise [NotDefinedError] if policy could not be determined # # @since v0.1.0 def policy! policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`" end # @return [String] the name of the key this object would have in a params hash # # @since v1.1.0 def param_key # rubocop:disable Metrics/AbcSize model = object.is_a?(Array) ? object.last : object if model.respond_to?(:model_name) model.model_name.param_key.to_s elsif model.is_a?(Class) model.to_s.demodulize.underscore else model.class.to_s.demodulize.underscore end end private # Given an object, find the policy class name. # # Uses recursion to handle namespaces. # # @return [String, Class] the policy class, or its name. # @since v0.2.0 def find(subject) if subject.is_a?(Array) modules = subject.dup last = modules.pop context = modules.map { |x| find_class_name(x) }.join("::") [context, find(last)].join("::") elsif subject.respond_to?(:policy_class) subject.policy_class elsif subject.class.respond_to?(:policy_class) subject.class.policy_class else klass = find_class_name(subject) "#{klass}#{SUFFIX}" end end # Given an object, find its' class name. # # - Supports ActiveModel. # - Supports regular classes. # - Supports symbols. # - Supports object instances. # # @return [String, Class] the class, or its name. # @since v1.1.0 def find_class_name(subject) if subject.respond_to?(:model_name) subject.model_name elsif subject.class.respond_to?(:model_name) subject.class.model_name elsif subject.is_a?(Class) subject elsif subject.is_a?(Symbol) subject.to_s.camelize else subject.class end end end end ================================================ FILE: lib/pundit/railtie.rb ================================================ # frozen_string_literal: true module Pundit # @since v2.5.0 class Railtie < Rails::Railtie if Rails.version.to_f >= 8.0 initializer "pundit.stats_directories" do require "rails/code_statistics" if Rails.root.join("app/policies").directory? Rails::CodeStatistics.register_directory("Policies", "app/policies") end if Rails.root.join("test/policies").directory? Rails::CodeStatistics.register_directory("Policy tests", "test/policies", test_directory: true) end end end end end ================================================ FILE: lib/pundit/rspec.rb ================================================ # frozen_string_literal: true require "pundit" # Array#to_sentence require "active_support/core_ext/array/conversions" module Pundit # Namespace for Pundit's RSpec integration. # @since v0.1.0 module RSpec # Namespace for Pundit's RSpec matchers. module Matchers extend ::RSpec::Matchers::DSL # @!method description=(description) class << self # Used to build a suitable description for the Pundit `permit` matcher. # @api public # @param value [String, Proc] # @example # Pundit::RSpec::Matchers.description = ->(user, record) do # "permit user with role #{user.role} to access record with ID #{record.id}" # end attr_writer :description # Used to retrieve a suitable description for the Pundit `permit` matcher. # @api private # @private def description(user, record) return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call) @description end end # rubocop:disable Metrics/BlockLength matcher :permit do |user, record| match_proc = lambda do |policy| @violating_permissions = permissions.find_all do |permission| !policy.new(user, record).public_send(permission) end @violating_permissions.empty? end match_when_negated_proc = lambda do |policy| @violating_permissions = permissions.find_all do |permission| policy.new(user, record).public_send(permission) end @violating_permissions.empty? end failure_message_proc = lambda do |policy| "Expected #{policy} to grant #{permissions.to_sentence} on " \ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted" end failure_message_when_negated_proc = lambda do |policy| "Expected #{policy} not to grant #{permissions.to_sentence} on " \ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted" end def was_or_were if @violating_permissions.count > 1 "were" else "was" end end description do Pundit::RSpec::Matchers.description(user, record) || super() end if respond_to?(:match_when_negated) match(&match_proc) match_when_negated(&match_when_negated_proc) failure_message(&failure_message_proc) failure_message_when_negated(&failure_message_when_negated_proc) else # :nocov: # Compatibility with RSpec < 3.0, released 2014-06-01. match_for_should(&match_proc) match_for_should_not(&match_when_negated_proc) failure_message_for_should(&failure_message_proc) failure_message_for_should_not(&failure_message_when_negated_proc) # :nocov: end if ::RSpec.respond_to?(:current_example) def current_example ::RSpec.current_example end else # :nocov: # Compatibility with RSpec < 3.0, released 2014-06-01. def current_example example end # :nocov: end def permissions current_example.metadata.fetch(:permissions) do raise KeyError, <<~ERROR.strip No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`? ERROR end end end # rubocop:enable Metrics/BlockLength end # Mixed in to all policy example groups to provide a DSL. module DSL # @example # describe PostPolicy do # permissions :show?, :update? do # it { is_expected.to permit(user, own_post) } # end # end # # @example focused example group # describe PostPolicy do # permissions :show?, :update?, :focus do # it { is_expected.to permit(user, own_post) } # end # end # # @param list [Symbol, Array] a permission to describe # @return [void] def permissions(*list, &block) metadata = {permissions: list, caller: caller} if list.last == :focus list.pop metadata[:focus] = true end description = list.to_sentence describe(description, metadata) { instance_eval(&block) } end end # Mixed in to all policy example groups. # # @private not useful module PolicyExampleGroup include Pundit::RSpec::Matchers def self.included(base) base.metadata[:type] = :policy base.extend Pundit::RSpec::DSL super end end end end RSpec.configure do |config| config.include(Pundit::RSpec::PolicyExampleGroup, file_path: %r{spec/policies}) config.include(Pundit::RSpec::PolicyExampleGroup, type: :policy) end ================================================ FILE: lib/pundit/version.rb ================================================ # frozen_string_literal: true module Pundit # The current version of Pundit. VERSION = "2.5.2" end ================================================ FILE: lib/pundit.rb ================================================ # frozen_string_literal: true require "active_support" require "pundit/version" require "pundit/error" require "pundit/policy_finder" require "pundit/context" require "pundit/authorization" require "pundit/helper" require "pundit/cache_store" require "pundit/cache_store/null_store" require "pundit/cache_store/legacy_store" # :nocov: require "pundit/railtie" if defined?(Rails) # :nocov: # Hello? Yes, this is Pundit. # # @api public module Pundit # @api private # @since v1.0.0 # @deprecated See {Pundit::PolicyFinder} SUFFIX = Pundit::PolicyFinder::SUFFIX # @api private # @private # @since v0.1.0 module Generators; end def self.included(base) location = caller_locations(1, 1).first warn <<~WARNING 'include Pundit' is deprecated. Please use 'include Pundit::Authorization' instead. (called from #{location.label} at #{location.path}:#{location.lineno}) WARNING base.include Authorization end class << self # @see Pundit::Context#authorize # @since v1.0.0 def authorize(user, record, query, policy_class: nil, cache: nil) context = if cache policy_cache = CacheStore::LegacyStore.new(cache) Context.new(user: user, policy_cache: policy_cache) else Context.new(user: user) end context.authorize(record, query: query, policy_class: policy_class) end # @see Pundit::Context#policy_scope # @since v0.1.0 def policy_scope(user, *args, **kwargs, &block) Context.new(user: user).policy_scope(*args, **kwargs, &block) end # @see Pundit::Context#policy_scope! # @since v0.1.0 def policy_scope!(user, *args, **kwargs, &block) Context.new(user: user).policy_scope!(*args, **kwargs, &block) end # @see Pundit::Context#policy # @since v0.1.0 def policy(user, *args, **kwargs, &block) Context.new(user: user).policy(*args, **kwargs, &block) end # @see Pundit::Context#policy! # @since v0.1.0 def policy!(user, *args, **kwargs, &block) Context.new(user: user).policy!(*args, **kwargs, &block) end end end ================================================ FILE: pundit.gemspec ================================================ # frozen_string_literal: true require_relative "lib/pundit/version" Gem::Specification.new do |gem| gem.name = "pundit" gem.version = Pundit::VERSION gem.authors = ["Jonas Nicklas", "Varvet AB"] gem.email = ["jonas.nicklas@gmail.com", "info@varvet.com"] gem.description = "Object oriented authorization for Rails applications" gem.summary = "OO authorization for Rails" gem.homepage = "https://github.com/varvet/pundit" gem.license = "MIT" Dir.chdir(__dir__) do gem.files = `git ls-files -z`.split("\x0").select do |f| f.start_with?("lib/", "README", "SECURITY", "LICENSE", "CHANGELOG", "CONTRIBUTING", "config/rubocop-rspec.yml") end end gem.require_paths = ["lib"] gem.metadata = { "rubygems_mfa_required" => "true", "bug_tracker_uri" => "https://github.com/varvet/pundit/issues", "changelog_uri" => "https://github.com/varvet/pundit/blob/main/CHANGELOG.md", "documentation_uri" => "https://github.com/varvet/pundit/blob/main/README.md", "homepage_uri" => "https://github.com/varvet/pundit", "source_code_uri" => "https://github.com/varvet/pundit" } gem.add_dependency "activesupport", ">= 3.0.0" end ================================================ FILE: spec/authorization_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "action_controller" RSpec.describe Pundit::Authorization do def to_params(*args, **kwargs, &block) ActionController::Parameters.new(*args, **kwargs, &block) end let(:controller) { Controller.new(user, "update", to_params({})) } let(:user) { double("user") } let(:post) { Post.new(user) } let(:comment) { Comment.new } let(:article) { Article.new } let(:article_tag) { ArticleTag.new } let(:wiki) { Wiki.new } describe "#verify_authorized" do it "does nothing when authorized" do controller.authorize(post) controller.verify_authorized end it "raises an exception when not authorized" do expect { controller.verify_authorized }.to raise_error(Pundit::AuthorizationNotPerformedError) end end describe "#verify_policy_scoped" do it "does nothing when policy_scope is used" do controller.policy_scope(Post) controller.verify_policy_scoped end it "raises an exception when policy_scope is not used" do expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError) end end describe "#pundit_policy_authorized?" do it "is true when authorized" do controller.authorize(post) expect(controller.pundit_policy_authorized?).to be true end it "is false when not authorized" do expect(controller.pundit_policy_authorized?).to be false end end describe "#pundit_policy_scoped?" do it "is true when policy_scope is used" do controller.policy_scope(Post) expect(controller.pundit_policy_scoped?).to be true end it "is false when policy scope is not used" do expect(controller.pundit_policy_scoped?).to be false end end describe "#authorize" do it "infers the policy name and authorizes based on it" do expect(controller.authorize(post)).to be_truthy end it "returns the record on successful authorization" do expect(controller.authorize(post)).to eq(post) end it "returns the record when passed record with namespace " do expect(controller.authorize([:project, comment], :update?)).to eq(comment) end it "returns the record when passed record with nested namespace " do expect(controller.authorize([:project, :admin, comment], :update?)).to eq(comment) end it "returns the policy name symbol when passed record with headless policy" do expect(controller.authorize(:publication, :create?)).to eq(:publication) end it "returns the class when passed record not a particular instance" do expect(controller.authorize(Post, :show?)).to eq(Post) end it "can be given a different permission to check" do expect(controller.authorize(post, :show?)).to be_truthy expect { controller.authorize(post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "can be given a different policy class" do expect(controller.authorize(post, :create?, policy_class: PublicationPolicy)).to be_truthy end it "works with anonymous class policies" do expect(controller.authorize(article_tag, :show?)).to be_truthy expect { controller.authorize(article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "throws an exception when the permission check fails" do expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError) end it "throws an exception when a policy cannot be found" do expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError) end it "caches the policy" do expect(controller.policies[post]).to be_nil controller.authorize(post) expect(controller.policies[post]).not_to be_nil end it "raises an error when the given record is nil" do expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "raises an error with a invalid policy constructor" do expect { controller.authorize(wiki, :destroy?) }.to raise_error(Pundit::InvalidConstructorError) end end describe "#skip_authorization" do it "disables authorization verification" do controller.skip_authorization expect { controller.verify_authorized }.not_to raise_error end end describe "#skip_policy_scope" do it "disables policy scope verification" do controller.skip_policy_scope expect { controller.verify_policy_scoped }.not_to raise_error end end describe "#pundit_user" do it "returns the same thing as current_user" do expect(controller.pundit_user).to eq controller.current_user end end describe "#policy" do it "returns an instantiated policy" do policy = controller.policy(post) expect(policy.user).to eq user expect(policy.post).to eq post end it "throws an exception if the given policy can't be found" do expect { controller.policy(article) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy constructor" do expect { controller.policy(wiki) }.to raise_error(Pundit::InvalidConstructorError) end it "allows policy to be injected" do new_policy = double controller.policies[post] = new_policy expect(controller.policy(post)).to eq new_policy end end describe "#policy_scope" do it "returns an instantiated policy scope" do expect(controller.policy_scope(Post)).to eq :published end it "allows policy scope class to be overridden" do expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published end it "throws an exception if the given policy can't be found" do expect { controller.policy_scope(Article) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy scope constructor" do expect { controller.policy_scope(Wiki) }.to raise_error(Pundit::InvalidConstructorError) end it "allows policy_scope to be injected" do new_scope = double controller.policy_scopes[Post] = new_scope expect(controller.policy_scope(Post)).to eq new_scope end end describe "#permitted_attributes" do it "checks policy for permitted attributes" do params = to_params( post: { title: "Hello", votes: 5, admin: true } ) action = "update" expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).permitted_attributes(post).to_h).to eq("votes" => 5) end it "checks policy for permitted attributes for record of a ActiveModel type" do customer_post = Customer::Post.new(user) params = to_params( customer_post: { title: "Hello", votes: 5, admin: true } ) action = "update" expect(Controller.new(user, action, params).permitted_attributes(customer_post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).permitted_attributes(customer_post).to_h).to eq( "votes" => 5 ) end it "goes through the policy cache" do params = to_params(post: {title: "Hello"}) user = double post = Post.new(user) controller = Controller.new(user, "update", params) expect do expect(controller.permitted_attributes(post)).to be_truthy expect(controller.permitted_attributes(post)).to be_truthy end.to change { PostPolicy.instances }.by(1) end end describe "#permitted_attributes_for_action" do it "is checked if it is defined in the policy" do params = to_params( post: { title: "Hello", body: "blah", votes: 5, admin: true } ) action = "revise" expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah") end it "can be explicitly set" do params = to_params( post: { title: "Hello", body: "blah", votes: 5, admin: true } ) action = "update" expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah") end end if ActionController::Parameters.method_defined?(:expect) describe "#expected_attributes" do it "checks policy for expected attributes" do params = to_params( post: { title: "Hello", votes: 5, admin: true } ) action = "update" expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).expected_attributes(post).to_h).to eq("votes" => 5) end it "checks policy for expected attributes for record of a ActiveModel type" do customer_post = Customer::Post.new(user) params = to_params( customer_post: { title: "Hello", votes: 5, admin: true } ) action = "update" expect(Controller.new(user, action, params).expected_attributes(customer_post).to_h).to eq( "title" => "Hello", "votes" => 5 ) expect(Controller.new(double, action, params).expected_attributes(customer_post).to_h).to eq( "votes" => 5 ) end it "goes through the policy cache" do params = to_params(post: {title: "Hello"}) user = double post = Post.new(user) controller = Controller.new(user, "update", params) expect do expect(controller.expected_attributes(post)).to be_truthy expect(controller.expected_attributes(post)).to be_truthy end.to change { PostPolicy.instances }.by(1) end end context "action-specific expected attributes" do it "is checked if it is defined in the policy" do params = to_params( post: { title: "Hello", body: "blah", votes: 5, admin: true } ) action = "revise" expect(Controller.new(user, action, params).expected_attributes(post).to_h).to eq("body" => "blah") end it "can be explicitly set" do params = to_params( post: { title: "Hello", body: "blah", votes: 5, admin: true } ) action = "update" controller = Controller.new(user, action, params) expect(controller.expected_attributes(post, action: :revise).to_h).to eq("body" => "blah") end end it "can be retrieved with an explicit param key" do params = to_params(admin_post: {title: "Hello"}) action = "update" controller = Controller.new(user, action, params) expect(controller.expected_attributes(post, param_key: "admin_post").to_h).to eq("title" => "Hello") end end describe "#pundit_reset!" do it "allows authorize to react to a user change" do expect(controller.authorize(post)).to be_truthy controller.current_user = double controller.pundit_reset! expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError) end it "allows policy to react to a user change" do expect(controller.policy(DummyCurrentUser).user).to be user new_user = double("new user") controller.current_user = new_user controller.pundit_reset! expect(controller.policy(DummyCurrentUser).user).to be new_user end it "allows policy scope to react to a user change" do expect(controller.policy_scope(DummyCurrentUser)).to be user new_user = double("new user") controller.current_user = new_user controller.pundit_reset! expect(controller.policy_scope(DummyCurrentUser)).to be new_user end it "resets the pundit context" do expect(controller.pundit.user).to be(user) new_user = double controller.current_user = new_user expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user) end it "clears pundit_policy_authorized? flag" do expect(controller.pundit_policy_authorized?).to be false controller.skip_authorization expect(controller.pundit_policy_authorized?).to be true controller.pundit_reset! expect(controller.pundit_policy_authorized?).to be false end it "clears pundit_policy_scoped? flag" do expect(controller.pundit_policy_scoped?).to be false controller.skip_policy_scope expect(controller.pundit_policy_scoped?).to be true controller.pundit_reset! expect(controller.pundit_policy_scoped?).to be false end end end ================================================ FILE: spec/generators_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "tmpdir" require "rails/generators" require "generators/pundit/install/install_generator" require "generators/pundit/policy/policy_generator" RSpec.describe "generators" do before(:all) do @tmpdir = Dir.mktmpdir Dir.chdir(@tmpdir) do Pundit::Generators::InstallGenerator.new([], {quiet: true}).invoke_all Pundit::Generators::PolicyGenerator.new(%w[Widget], {quiet: true}).invoke_all require "./app/policies/application_policy" require "./app/policies/widget_policy" end end after(:all) do FileUtils.remove_entry(@tmpdir) end describe "WidgetPolicy", type: :policy do permissions :index?, :show?, :create?, :new?, :update?, :edit?, :destroy? do it "has safe defaults" do expect(WidgetPolicy).not_to permit(double("User"), double("Widget")) end end describe "WidgetPolicy::Scope" do describe "#resolve" do it "raises a descriptive error" do scope = WidgetPolicy::Scope.new(double("User"), double("User.all")) expect { scope.resolve }.to raise_error(NoMethodError, /WidgetPolicy::Scope/) end end end end end ================================================ FILE: spec/policies/post_policy_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe PostPolicy do let(:user) { double } let(:own_post) { double(user: user) } let(:other_post) { double(user: double) } subject { described_class } permissions :update?, :show? do it "is successful when all permissions match" do is_expected.to permit(user, own_post) end it "fails when any permissions do not match" do expect do is_expected.to permit(user, other_post) end.to raise_error(RSpec::Expectations::ExpectationNotMetError) end it "uses the default description if not overridden" do expect(permit(user, own_post).description).to eq("permit #{user.inspect} and #{own_post.inspect}") end context "when the matcher description is overridden" do after do Pundit::RSpec::Matchers.description = nil end it "sets a custom matcher description with a Proc" do allow(user).to receive(:role).and_return("default_role") allow(own_post).to receive(:id).and_return(1) Pundit::RSpec::Matchers.description = lambda { |user, record| "permit user with role #{user.role} to access record with ID #{record.id}" } description = permit(user, own_post).description expect(description).to eq("permit user with role default_role to access record with ID 1") end it "sets a custom matcher description with a string" do Pundit::RSpec::Matchers.description = "permit user" expect(permit(user, own_post).description).to eq("permit user") end end end end ================================================ FILE: spec/policy_finder_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Pundit::PolicyFinder do let(:user) { double } let(:post) { Post.new(user) } let(:comment) { CommentFourFiveSix.new } let(:article) { Article.new } describe "SUFFIX" do specify { expect(described_class::SUFFIX).to eq "Policy" } specify { expect(Pundit::SUFFIX).to eq(described_class::SUFFIX) } end describe "#scope" do subject { described_class.new(post) } it "returns a policy scope" do expect(subject.scope).to eq PostPolicy::Scope end context "policy is nil" do it "returns nil" do allow(subject).to receive(:policy).and_return nil expect(subject.scope).to eq nil end end end describe "#policy" do context "with an instance" do it "returns the associated policy" do object = described_class.new(post) expect(object.policy).to eq PostPolicy end end context "with an array of symbols" do it "returns the associated namespaced policy" do object = described_class.new(%i[project post]) expect(object.policy).to eq Project::PostPolicy end end context "with an array of a symbol and an instance" do it "returns the associated namespaced policy" do object = described_class.new([:project, post]) expect(object.policy).to eq Project::PostPolicy end end context "with an array of a symbol and a class with a specified policy class" do it "returns the associated namespaced policy" do object = described_class.new([:project, Customer::Post]) expect(object.policy).to eq Project::PostPolicy end end context "with an array of a symbol and a class with a specified model name" do it "returns the associated namespaced policy" do object = described_class.new([:project, CommentsRelation]) expect(object.policy).to eq Project::CommentPolicy end end context "with a class" do it "returns the associated policy" do object = described_class.new(Post) expect(object.policy).to eq PostPolicy end end context "with a class which has a specified policy class" do it "returns the associated policy" do object = described_class.new(Customer::Post) expect(object.policy).to eq PostPolicy end end context "with an instance which has a specified policy class" do it "returns the associated policy" do object = described_class.new(Customer::Post.new(user)) expect(object.policy).to eq PostPolicy end end context "with a class which has a specified model name" do it "returns the associated policy" do object = described_class.new(CommentsRelation) expect(object.policy).to eq CommentPolicy end end context "with an instance which has a specified policy class" do it "returns the associated policy" do object = described_class.new(CommentsRelation.new) expect(object.policy).to eq CommentPolicy end end context "with nil" do it "returns a NilClassPolicy" do object = described_class.new(nil) expect(object.policy).to eq NilClassPolicy end end context "with a class that doesn't have an associated policy" do it "returns nil" do object = described_class.new(Foo) expect(object.policy).to eq nil end end end describe "#scope!" do context "@object is nil" do subject { described_class.new(nil) } it "returns the NilClass policy's scope class" do expect(subject.scope!).to eq NilClassPolicy::Scope end end context "@object is defined" do subject { described_class.new(post) } it "returns the scope" do expect(subject.scope!).to eq PostPolicy::Scope end end end describe "#param_key" do context "object responds to model_name" do subject { described_class.new(comment) } it "returns the param_key" do expect(subject.object).to respond_to(:model_name) expect(subject.param_key).to eq "comment_four_five_six" end end context "object is a class" do subject { described_class.new(Article) } it "returns the param_key" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).to be_a Class expect(subject.param_key).to eq "article" end end context "object is an instance of a class" do subject { described_class.new(article) } it "returns the param_key" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).not_to be_a Class expect(subject.object).to be_an_instance_of Article expect(subject.param_key).to eq "article" end end context "object is an array" do subject { described_class.new([:project, article]) } it "returns the param_key for the last element of the array" do expect(subject.object).not_to respond_to(:model_name) expect(subject.object).not_to be_a Class expect(subject.object).to be_an_instance_of Array expect(subject.param_key).to eq "article" end end end end ================================================ FILE: spec/pundit/helper_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Pundit::Helper do let(:user) { double } let(:controller) { Controller.new(user, "update", double) } let(:view) { Controller::View.new(controller) } describe "#policy_scope" do it "doesn't flip pundit_policy_scoped?" do scoped = view.policy_scope(Post) expect(scoped).to be(Post.published) expect(controller).not_to be_pundit_policy_scoped end end end ================================================ FILE: spec/pundit_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Pundit do let(:user) { double } let(:post) { Post.new(user) } let(:customer_post) { Customer::Post.new(user) } let(:post_four_five_six) { PostFourFiveSix.new(user) } let(:comment) { Comment.new } let(:comment_four_five_six) { CommentFourFiveSix.new } let(:article) { Article.new } let(:artificial_blog) { ArtificialBlog.new } let(:article_tag) { ArticleTag.new } let(:comments_relation) { CommentsRelation.new(empty: false) } let(:empty_comments_relation) { CommentsRelation.new(empty: true) } let(:tag_four_five_six) { ProjectOneTwoThree::TagFourFiveSix.new(user) } let(:avatar_four_five_six) { ProjectOneTwoThree::AvatarFourFiveSix.new } let(:wiki) { Wiki.new } describe ".authorize" do it "infers the policy and authorizes based on it" do expect(Pundit.authorize(user, post, :update?)).to be_truthy end it "returns the record on successful authorization" do expect(Pundit.authorize(user, post, :update?)).to eq(post) end it "returns the record when passed record with namespace " do expect(Pundit.authorize(user, [:project, comment], :update?)).to eq(comment) end it "returns the record when passed record with nested namespace " do expect(Pundit.authorize(user, [:project, :admin, comment], :update?)).to eq(comment) end it "returns the policy name symbol when passed record with headless policy" do expect(Pundit.authorize(user, :publication, :create?)).to eq(:publication) end it "returns the class when passed record not a particular instance" do expect(Pundit.authorize(user, Post, :show?)).to eq(Post) end it "works with anonymous class policies" do expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) end it "raises an error with the policy, query and record" do # rubocop:disable Style/MultilineBlockChain expect do Pundit.authorize(user, post, :destroy?) end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? this Post") do |error| expect(error.query).to eq :destroy? expect(error.record).to eq post expect(error.policy).to have_attributes( user: user, record: post ) expect(error.policy).to be_a(PostPolicy) end # rubocop:enable Style/MultilineBlockChain end it "raises an error with the policy, query and record when the record is namespaced" do # rubocop:disable Style/MultilineBlockChain expect do Pundit.authorize(user, [:project, :admin, comment], :destroy?) end.to raise_error(Pundit::NotAuthorizedError, "not allowed to Project::Admin::CommentPolicy#destroy? this Comment") do |error| expect(error.query).to eq :destroy? expect(error.record).to eq comment expect(error.policy).to have_attributes( user: user, record: comment ) expect(error.policy).to be_a(Project::Admin::CommentPolicy) end # rubocop:enable Style/MultilineBlockChain end it "raises an error with the policy, query and the class name when a Class is given" do # rubocop:disable Style/MultilineBlockChain expect do Pundit.authorize(user, Post, :destroy?) end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? Post") do |error| expect(error.query).to eq :destroy? expect(error.record).to eq Post expect(error.policy).to have_attributes( user: user, record: Post ) expect(error.policy).to be_a(PostPolicy) end # rubocop:enable Style/MultilineBlockChain end it "raises an error with a invalid policy constructor" do expect do Pundit.authorize(user, wiki, :update?) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end context "when passed a policy class" do it "uses the passed policy class" do expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy end # This is documenting past behaviour. it "doesn't cache the policy class" do cache = {} expect do Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache) Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache) end.to change { PublicationPolicy.instances }.by(2) end end context "when passed a policy class while simultaenously passing a namespace" do it "uses the passed policy class" do expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy end end context "when passed an explicit cache" do it "uses the hash assignment interface on the cache" do custom_cache = CustomCache.new Pundit.authorize(user, post, :update?, cache: custom_cache) expect(custom_cache.to_h).to match({ post => kind_of(PostPolicy) }) end end end describe ".policy_scope" do it "returns an instantiated policy scope given a plain model class" do expect(Pundit.policy_scope(user, Post)).to eq :published end it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope(user, Comment)).to eq CommentScope.new(Comment) end it "returns an instantiated policy scope given an active record relation" do expect(Pundit.policy_scope(user, comments_relation)).to eq CommentScope.new(comments_relation) end it "returns an instantiated policy scope given an empty active record relation" do expect(Pundit.policy_scope(user, empty_comments_relation)).to eq CommentScope.new(empty_comments_relation) end it "returns an instantiated policy scope given an array of a symbol and plain model class" do expect(Pundit.policy_scope(user, [:project, Post])).to eq :read end it "returns an instantiated policy scope given an array of a symbol and active model class" do expect(Pundit.policy_scope(user, [:project, Comment])).to eq Comment end it "returns nil if the given policy scope can't be found" do expect(Pundit.policy_scope(user, Article)).to be_nil end it "raises an exception if nil object given" do expect { Pundit.policy_scope(user, nil) }.to raise_error(Pundit::NotDefinedError) end it "raises an error with a invalid policy scope constructor" do expect do Pundit.policy_scope(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end it "raises an original error with a policy scope that contains error" do expect do Pundit.policy_scope(user, DefaultScopeContainsError) end.to raise_error(RuntimeError, "This is an arbitrary error that should bubble up") end end describe ".policy_scope!" do it "returns an instantiated policy scope given a plain model class" do expect(Pundit.policy_scope!(user, Post)).to eq :published end it "returns an instantiated policy scope given an active model class" do expect(Pundit.policy_scope!(user, Comment)).to eq CommentScope.new(Comment) end it "throws an exception if the given policy scope can't be found" do expect { Pundit.policy_scope!(user, Article) }.to raise_error(Pundit::NotDefinedError) end it "throws an exception if the given policy scope can't be found" do expect { Pundit.policy_scope!(user, ArticleTag) }.to raise_error(Pundit::NotDefinedError) end it "throws an exception if the given policy scope is nil" do expect do Pundit.policy_scope!(user, nil) end.to raise_error(Pundit::NotDefinedError, "Cannot scope NilClass") end it "returns an instantiated policy scope given an array of a symbol and plain model class" do expect(Pundit.policy_scope!(user, [:project, Post])).to eq :read end it "returns an instantiated policy scope given an array of a symbol and active model class" do expect(Pundit.policy_scope!(user, [:project, Comment])).to eq Comment end it "raises an error with a invalid policy scope constructor" do expect do Pundit.policy_scope(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe ".policy" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy(user, post) expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an active model instance" do policy = Pundit.policy(user, comment) expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy(user, Post) expect(policy.user).to eq user expect(policy.post).to eq Post end it "returns an instantiated policy given an active model class" do policy = Pundit.policy(user, Comment) expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given a symbol" do policy = Pundit.policy(user, :criteria) expect(policy.class).to eq CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of symbols" do policy = Pundit.policy(user, %i[project criteria]) expect(policy.class).to eq Project::CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of a symbol and plain model instance" do policy = Pundit.policy(user, [:project, post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an array of a symbol and a model instance with policy_class override" do policy = Pundit.policy(user, [:project, customer_post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq customer_post end it "returns an instantiated policy given an array of a symbol and an active model instance" do policy = Pundit.policy(user, [:project, comment]) expect(policy.class).to eq Project::CommentPolicy expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given an array of a symbol and a plain model class" do policy = Pundit.policy(user, [:project, Post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq Post end it "raises an error with a invalid policy constructor" do expect do Pundit.policy(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end it "returns an instantiated policy given an array of a symbol and an active model class" do policy = Pundit.policy(user, [:project, Comment]) expect(policy.class).to eq Project::CommentPolicy expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given an array of a symbol and a class with policy_class override" do policy = Pundit.policy(user, [:project, Customer::Post]) expect(policy.class).to eq Project::PostPolicy expect(policy.user).to eq user expect(policy.post).to eq Customer::Post end it "returns correct policy class for an array of a multi-word symbols" do policy = Pundit.policy(user, %i[project_one_two_three criteria_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::CriteriaFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model instance" do policy = Pundit.policy(user, [:project_one_two_three, post_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word active model instance" do policy = Pundit.policy(user, [:project_one_two_three, comment_four_five_six]) expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model class" do policy = Pundit.policy(user, [:project_one_two_three, PostFourFiveSix]) expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy end it "returns correct policy class for an array of a multi-word symbol and a multi-word active model class" do policy = Pundit.policy(user, [:project_one_two_three, CommentFourFiveSix]) expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped plain model class" do policy = Pundit.policy(user, ProjectOneTwoThree::TagFourFiveSix) expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped plain model instance" do policy = Pundit.policy(user, tag_four_five_six) expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped active model class" do policy = Pundit.policy(user, ProjectOneTwoThree::AvatarFourFiveSix) expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy end it "returns correct policy class for a multi-word scoped active model instance" do policy = Pundit.policy(user, avatar_four_five_six) expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy end it "returns nil if the given policy can't be found" do expect(Pundit.policy(user, article)).to be_nil expect(Pundit.policy(user, Article)).to be_nil end it "returns the specified NilClassPolicy for nil" do expect(Pundit.policy(user, nil)).to be_a NilClassPolicy end describe "with .policy_class set on the model" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy(user, artificial_blog) expect(policy.user).to eq user expect(policy.blog).to eq artificial_blog end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy(user, ArtificialBlog) expect(policy.user).to eq user expect(policy.blog).to eq ArtificialBlog end it "returns an instantiated policy given a plain model instance providing an anonymous class" do policy = Pundit.policy(user, article_tag) expect(policy.user).to eq user expect(policy.tag).to eq article_tag end it "returns an instantiated policy given a plain model class providing an anonymous class" do policy = Pundit.policy(user, ArticleTag) expect(policy.user).to eq user expect(policy.tag).to eq ArticleTag end end end describe ".policy!" do it "returns an instantiated policy given a plain model instance" do policy = Pundit.policy!(user, post) expect(policy.user).to eq user expect(policy.post).to eq post end it "returns an instantiated policy given an active model instance" do policy = Pundit.policy!(user, comment) expect(policy.user).to eq user expect(policy.comment).to eq comment end it "returns an instantiated policy given a plain model class" do policy = Pundit.policy!(user, Post) expect(policy.user).to eq user expect(policy.post).to eq Post end it "returns an instantiated policy given an active model class" do policy = Pundit.policy!(user, Comment) expect(policy.user).to eq user expect(policy.comment).to eq Comment end it "returns an instantiated policy given a symbol" do policy = Pundit.policy!(user, :criteria) expect(policy.class).to eq CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "returns an instantiated policy given an array of symbols" do policy = Pundit.policy!(user, %i[project criteria]) expect(policy.class).to eq Project::CriteriaPolicy expect(policy.user).to eq user expect(policy.criteria).to eq :criteria end it "throws an exception if the given policy can't be found" do expect { Pundit.policy!(user, article) }.to raise_error(Pundit::NotDefinedError) expect { Pundit.policy!(user, Article) }.to raise_error(Pundit::NotDefinedError) end it "returns the specified NilClassPolicy for nil" do expect(Pundit.policy!(user, nil)).to be_a NilClassPolicy end it "raises an error with a invalid policy constructor" do expect do Pundit.policy(user, Wiki) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end end describe ".included" do it "includes Authorization module" do klass = Class.new expect do klass.include Pundit end.to output.to_stderr expect(klass).to include Pundit::Authorization end it "warns about deprecation" do klass = Class.new expect do klass.include Pundit end.to output(a_string_starting_with("'include Pundit' is deprecated")).to_stderr end end describe "Pundit::NotAuthorizedError" do it "can be initialized with a string as message" do error = Pundit::NotAuthorizedError.new("must be logged in") expect(error.message).to eq "must be logged in" end end end ================================================ FILE: spec/rspec_dsl_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Pundit RSpec DSL", type: :policy do let(:fake_rspec) do double = class_double(RSpec::Core::ExampleGroup) double.extend(::Pundit::RSpec::DSL) double end let(:block) { proc { "block content" } } let(:user) { double } let(:other_user) { double } let(:post) { Post.new(user) } let(:policy) { PostPolicy } it "calls describe with the correct metadata and without :focus" do expected_metadata = {permissions: %i[item1 item2], caller: instance_of(Array)} expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| expect(block.call).to eq("block content") end fake_rspec.permissions(:item1, :item2, &block) end it "calls describe with the correct metadata and with :focus" do expected_metadata = {permissions: %i[item1 item2], caller: instance_of(Array), focus: true} expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| expect(block.call).to eq("block content") end fake_rspec.permissions(:item1, :item2, :focus, &block) end describe "#permit" do context "when not appropriately wrapped in permissions" do it "raises a descriptive error" do expect do expect(policy).to permit(user, post) end.to raise_error(KeyError, <<~MSG.strip) No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`? MSG end end permissions :edit?, :update? do it "succeeds when action is permitted" do expect(policy).to permit(user, post) end context "when it fails" do it "fails with a descriptive error message" do expect do expect(policy).to permit(other_user, post) end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip) Expected PostPolicy to grant edit? and update? on Post but edit? and update? were not granted MSG end end context "when negated" do it "succeeds when action is not permitted" do expect(policy).not_to permit(other_user, post) end context "when it fails" do it "fails with a descriptive error message" do expect do expect(policy).not_to permit(user, post) end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip) Expected PostPolicy not to grant edit? and update? on Post but edit? and update? were granted MSG end end end end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true if ENV["COVERAGE"] require "simplecov" require "simplecov_json_formatter" SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter ]) SimpleCov.start do enable_coverage :branch primary_coverage :branch end SimpleCov.minimum_coverage_by_file line: 100, branch: 100 end # @see https://github.com/rails/rails/issues/54260 require "logger" if RUBY_ENGINE == "jruby" && RUBY_ENGINE_VERSION.start_with?("9.3") require "pundit" require "pundit/rspec" require "active_model/naming" # Load all supporting files: models, policies, etc. require "zeitwerk" loader = Zeitwerk::Loader.new loader.push_dir(File.expand_path("support/models", __dir__)) loader.push_dir(File.expand_path("support/policies", __dir__)) loader.push_dir(File.expand_path("support/lib", __dir__)) loader.setup loader.eager_load ================================================ FILE: spec/support/lib/controller.rb ================================================ # frozen_string_literal: true class Controller attr_accessor :current_user attr_reader :action_name, :params class View def initialize(controller) @controller = controller end attr_reader :controller end class << self def helper(mod) View.include(mod) end def helper_method(method) View.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, **kwargs, &block) controller.send(:#{method}, *args, **kwargs, &block) end RUBY end end include Pundit::Authorization # Mark protected methods public so they may be called in test public(*Pundit::Authorization.protected_instance_methods) def initialize(current_user, action_name, params) @current_user = current_user @action_name = action_name @params = params end end ================================================ FILE: spec/support/lib/custom_cache.rb ================================================ # frozen_string_literal: true class CustomCache def initialize @store = {} end def to_h @store end def [](key) @store[key] end def []=(key, value) @store[key] = value end end ================================================ FILE: spec/support/lib/instance_tracking.rb ================================================ # frozen_string_literal: true module InstanceTracking module ClassMethods def instances @instances || 0 end attr_writer :instances end def self.prepended(other) other.extend(ClassMethods) end def initialize(*args, **kwargs, &block) self.class.instances += 1 super end end ================================================ FILE: spec/support/models/article.rb ================================================ # frozen_string_literal: true class Article end ================================================ FILE: spec/support/models/article_tag.rb ================================================ # frozen_string_literal: true class ArticleTag def self.policy_class ArticleTagOtherNamePolicy end end ================================================ FILE: spec/support/models/artificial_blog.rb ================================================ # frozen_string_literal: true class ArtificialBlog < Blog def self.policy_class BlogPolicy end end ================================================ FILE: spec/support/models/blog.rb ================================================ # frozen_string_literal: true class Blog end ================================================ FILE: spec/support/models/comment.rb ================================================ # frozen_string_literal: true class Comment extend ActiveModel::Naming end ================================================ FILE: spec/support/models/comment_four_five_six.rb ================================================ # frozen_string_literal: true class CommentFourFiveSix extend ActiveModel::Naming end ================================================ FILE: spec/support/models/comment_scope.rb ================================================ # frozen_string_literal: true class CommentScope attr_reader :original_object def initialize(original_object) @original_object = original_object end def ==(other) original_object == other.original_object end end ================================================ FILE: spec/support/models/comments_relation.rb ================================================ # frozen_string_literal: true class CommentsRelation def initialize(empty: false) @empty = empty end def self.model_name Comment.model_name end end ================================================ FILE: spec/support/models/customer/post.rb ================================================ # frozen_string_literal: true module Customer class Post < ::Post extend ActiveModel::Naming def self.policy_class PostPolicy end end end ================================================ FILE: spec/support/models/default_scope_contains_error.rb ================================================ # frozen_string_literal: true class DefaultScopeContainsError def self.all end end ================================================ FILE: spec/support/models/dummy_current_user.rb ================================================ # frozen_string_literal: true class DummyCurrentUser end ================================================ FILE: spec/support/models/foo.rb ================================================ # frozen_string_literal: true class Foo end ================================================ FILE: spec/support/models/post.rb ================================================ # frozen_string_literal: true class Post def initialize(user = nil) @user = user end attr_reader :user def self.published :published end def self.read :read end def to_s "Post" end end ================================================ FILE: spec/support/models/post_four_five_six.rb ================================================ # frozen_string_literal: true class PostFourFiveSix def initialize(user) @user = user end attr_reader(:user) end ================================================ FILE: spec/support/models/project_one_two_three/avatar_four_five_six.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class AvatarFourFiveSix extend ActiveModel::Naming end end ================================================ FILE: spec/support/models/project_one_two_three/tag_four_five_six.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class TagFourFiveSix def initialize(user) @user = user end attr_reader(:user) end end ================================================ FILE: spec/support/models/wiki.rb ================================================ # frozen_string_literal: true class Wiki end ================================================ FILE: spec/support/policies/article_tag_other_name_policy.rb ================================================ # frozen_string_literal: true class ArticleTagOtherNamePolicy < BasePolicy def show? true end def destroy? false end alias_method :tag, :record end ================================================ FILE: spec/support/policies/base_policy.rb ================================================ # frozen_string_literal: true class BasePolicy prepend InstanceTracking class BaseScope prepend InstanceTracking def initialize(user, scope) @user = user @scope = scope end attr_reader :user, :scope end def initialize(user, record) @user = user @record = record end attr_reader :user, :record end ================================================ FILE: spec/support/policies/blog_policy.rb ================================================ # frozen_string_literal: true class BlogPolicy < BasePolicy alias_method :blog, :record end ================================================ FILE: spec/support/policies/comment_policy.rb ================================================ # frozen_string_literal: true class CommentPolicy < BasePolicy class Scope < BaseScope def resolve CommentScope.new(scope) end end alias_method :comment, :record end ================================================ FILE: spec/support/policies/criteria_policy.rb ================================================ # frozen_string_literal: true class CriteriaPolicy < BasePolicy alias_method :criteria, :record end ================================================ FILE: spec/support/policies/default_scope_contains_error_policy.rb ================================================ # frozen_string_literal: true class DefaultScopeContainsErrorPolicy < BasePolicy class Scope < BaseScope def resolve # deliberate wrong usage of the method raise "This is an arbitrary error that should bubble up" end end end ================================================ FILE: spec/support/policies/dummy_current_user_policy.rb ================================================ # frozen_string_literal: true class DummyCurrentUserPolicy < BasePolicy class Scope < BasePolicy::BaseScope def resolve user end end end ================================================ FILE: spec/support/policies/nil_class_policy.rb ================================================ # frozen_string_literal: true class NilClassPolicy < BasePolicy class Scope def initialize(*) raise Pundit::NotDefinedError, "Cannot scope NilClass" end end def destroy? false end end ================================================ FILE: spec/support/policies/post_policy.rb ================================================ # frozen_string_literal: true class PostPolicy < BasePolicy class Scope < BaseScope def resolve scope.published end end alias_method :post, :record def update? post.user == user end alias_method :edit?, :update? def destroy? false end def show? true end def permitted_attributes if post.user == user %i[title votes] else [:votes] end end def permitted_attributes_for_revise [:body] end def expected_attributes_for_action(action_name) case action_name.to_sym when :revise [:body] else if post.user == user %i[title votes] else [:votes] end end end end ================================================ FILE: spec/support/policies/project/admin/comment_policy.rb ================================================ # frozen_string_literal: true module Project module Admin class CommentPolicy < BasePolicy def update? true end def destroy? false end end end end ================================================ FILE: spec/support/policies/project/comment_policy.rb ================================================ # frozen_string_literal: true module Project class CommentPolicy < BasePolicy class Scope < BaseScope def resolve scope end end def update? true end alias_method :comment, :record end end ================================================ FILE: spec/support/policies/project/criteria_policy.rb ================================================ # frozen_string_literal: true module Project class CriteriaPolicy < BasePolicy alias_method :criteria, :record end end ================================================ FILE: spec/support/policies/project/post_policy.rb ================================================ # frozen_string_literal: true module Project class PostPolicy < BasePolicy class Scope < BaseScope def resolve scope.read end end alias_method :post, :record end end ================================================ FILE: spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class AvatarFourFiveSixPolicy < BasePolicy end end ================================================ FILE: spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class CommentFourFiveSixPolicy < BasePolicy end end ================================================ FILE: spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class CriteriaFourFiveSixPolicy < BasePolicy end end ================================================ FILE: spec/support/policies/project_one_two_three/post_four_five_six_policy.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class PostFourFiveSixPolicy < BasePolicy end end ================================================ FILE: spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb ================================================ # frozen_string_literal: true module ProjectOneTwoThree class TagFourFiveSixPolicy < BasePolicy end end ================================================ FILE: spec/support/policies/publication_policy.rb ================================================ # frozen_string_literal: true class PublicationPolicy < BasePolicy class Scope < BaseScope def resolve scope.published end end def create? true end end ================================================ FILE: spec/support/policies/wiki_policy.rb ================================================ # frozen_string_literal: true class WikiPolicy class Scope # deliberate typo method def initalize end end end