From 55abb3ecd8face5e2cb28437c133e419e056204c Mon Sep 17 00:00:00 2001 From: Jim Gay Date: Wed, 10 Apr 2024 15:55:36 -0400 Subject: [PATCH] Initial pundit-plus implementation --- .github/dependabot.yml | 6 ++ .github/workflows/test.yml | 64 +++++++++++++ .gitignore | 9 ++ .rspec | 3 + .simplecov | 4 + CHANGELOG.md | 8 ++ Gemfile | 12 +++ Gemfile.lock | 133 +++++++++++++++++++++++++++ LICENSE | 21 +++++ README.md | 56 +++++++++++ Rakefile | 8 ++ bin/console | 11 +++ bin/setup | 8 ++ lib/pundit/plus.rb | 12 +++ lib/pundit/plus/custom_exception.rb | 24 +++++ lib/pundit/plus/version.rb | 7 ++ pundit-plus.gemspec | 28 ++++++ spec/pundit/custom_exception_spec.rb | 63 +++++++++++++ spec/spec_helper.rb | 88 ++++++++++++++++++ 19 files changed, 565 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .simplecov create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/console create mode 100755 bin/setup create mode 100644 lib/pundit/plus.rb create mode 100644 lib/pundit/plus/custom_exception.rb create mode 100644 lib/pundit/plus/version.rb create mode 100644 pundit-plus.gemspec create mode 100644 spec/pundit/custom_exception_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dede150 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'bundler' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1a16ae5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Testing + +on: + push: + branches-ignore: [master] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + version: ["3.2"] + env: + NOTIFY_SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + NOTIFY_SLACK_NOTIFY_CHANNEL: "oss-notices" + NOTIFY_CURRENT_REPOSITORY_URL: "${{ github.server_url }}/${{ github.repository }}" + NOTIFY_TEST_RUN_ID: "${{ github.run_id }}" + CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.version }} + bundler-cache: true + - name: Update Bundle + run: bundle check || bundle install + - name: Set ownership + run: | + # this is to fix GIT not liking owner of the checkout dir + git config --global --add safe.directory "$GITHUB_WORKSPACE" + - uses: amancevice/setup-code-climate@v1 + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + - run: cc-test-reporter before-build + - name: Test + run: bundle exec rake + - run: cc-test-reporter after-build + if: ${{ github.event_name != 'pull_request' }} + + linter: + name: Linter + runs-on: ubuntu-latest + strategy: + matrix: + version: ["3.2"] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.version }} + bundler-cache: true + - run: bundle exec standardrb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6050486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +spec/examples.txt \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..020b2d6 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--require debug +--order random \ No newline at end of file diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000..e938130 --- /dev/null +++ b/.simplecov @@ -0,0 +1,4 @@ +require "simplecov" +SimpleCov.start do + add_filter "/spec/" +end diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e9453d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [2024-04-10] + +### Added +- Initial project setup with basic Pundit integration. \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7130e7e --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "debug" +gem "rake" +gem "rspec" +gem "simplecov" +gem "yard" +gem "standard" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b55dda0 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,133 @@ +PATH + remote: . + specs: + pundit-plus (0.1.0) + pundit + pundit-matchers + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.3.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.7) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.5.1) + docile (1.4.0) + drb (2.2.1) + i18n (1.14.4) + concurrent-ruby (~> 1.0) + io-console (0.7.2) + irb (1.12.0) + rdoc + reline (>= 0.4.2) + json (2.7.2) + language_server-protocol (3.17.0.3) + lint_roller (1.1.0) + minitest (5.22.3) + mutex_m (0.2.0) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + psych (5.1.2) + stringio + pundit (2.3.1) + activesupport (>= 3.0.0) + pundit-matchers (3.1.2) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + racc (1.7.3) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.6.3.1) + psych (>= 4.0.0) + regexp_parser (2.9.0) + reline (0.5.1) + io-console (~> 0.5) + rexml (3.2.6) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + rubocop (1.62.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (1.13.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + standard (1.35.1) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.62.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.3) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.3.1) + lint_roller (~> 1.1) + rubocop-performance (~> 1.20.2) + stringio (3.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + yard (0.9.36) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + debug + pundit-plus! + rake + rspec + simplecov + standard + yard + +BUNDLED WITH + 2.5.6 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4bd0b9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 SOFware LLC + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dde6c5c --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Pundit::Plus + +Add some extra features to Pundit. + +## Installation + +Install the gem and add to the application's Gemfile by executing: + + $ bundle add pundit-plus + +If bundler is not being used to manage dependencies, install the gem by executing: + + $ gem install pundit-plus + +## Usage + +This includes [Pundit](https://github.com/varvet/pundit) and [Pundit Matchers](https://github.com/pundit-community/pundit-matchers). + +Follow the instructions for using Pundit and Pundit Matchers. + +Include the `Pundit::Plus` module in your policy classes to add extra features to Pundit. For example: + +```ruby +class ApplicationPolicy + include Pundit::Plus + # this module defines the default exception_from behavior +end + +class MyPolicy < ApplicationPolicy + # Then you can use your own exception classes + class CustomException < Pundit::NotAuthorizedError + def initialize(options = {}) + options[:message] ||= "You are not authorized to perform this action." + super(options) + end + end + + def exeception_from(query:) + if query == :show? + CustomException + else + super + end + end +end +``` + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/SOFware/pundit-plus. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b6ae734 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..dba234f --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "pundit/plus" + +# 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__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/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 diff --git a/lib/pundit/plus.rb b/lib/pundit/plus.rb new file mode 100644 index 0000000..c2fb559 --- /dev/null +++ b/lib/pundit/plus.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative "plus/version" +require_relative "plus/custom_exception" + +module Pundit + module Plus + def exception_from(query:) + Pundit::NotAuthorizedError + end + end +end diff --git a/lib/pundit/plus/custom_exception.rb b/lib/pundit/plus/custom_exception.rb new file mode 100644 index 0000000..8094c11 --- /dev/null +++ b/lib/pundit/plus/custom_exception.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "pundit" + +module Pundit + module Plus + # This module is prepended to Pundit to allow for custom exceptions to be + # raised by policies. + module CustomException + # This method is called by Pundit when a policy raises an exception. + # + # The default implementation raises the exception that was passed to + # `authorize`, but you can override this method in your policy classes to + # handle the exception differently. + # + # To make use of this method define the `exception_from` method in your + # policy class. + def raise(klass, query:, record:, policy:) + super(policy.exception_from(query:), query: query, record: record, policy: policy) + end + end + end +end +Pundit.singleton_class.prepend(Pundit::Plus::CustomException) diff --git a/lib/pundit/plus/version.rb b/lib/pundit/plus/version.rb new file mode 100644 index 0000000..f491131 --- /dev/null +++ b/lib/pundit/plus/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Pundit + module Plus + VERSION = "0.1.0" + end +end diff --git a/pundit-plus.gemspec b/pundit-plus.gemspec new file mode 100644 index 0000000..ddd8cb1 --- /dev/null +++ b/pundit-plus.gemspec @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "lib/pundit/plus/version" + +Gem::Specification.new do |spec| + spec.name = "pundit-plus" + spec.version = Pundit::Plus::VERSION + spec.authors = ["Jim Gay"] + spec.email = ["jim@saturnflyer.com"] + + spec.summary = "Pundit with additional features." + spec.description = "Add Pundit to your application with additional features." + spec.homepage = "https://github.com/SOFware/pundit-plus" + spec.required_ruby_version = ">= 3.0.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/SOFware/pundit-plus" + spec.metadata["changelog_uri"] = "https://github.com/SOFware/pundit-plus/blob/main/CHANGELOG.md" + + spec.files = Dir["lib/**/*", "LICENSE", "Rakefile", "README.md", "CHANGELOG.md"] + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + # Uncomment to register a new dependency of your gem + spec.add_dependency "pundit" + spec.add_dependency "pundit-matchers" +end diff --git a/spec/pundit/custom_exception_spec.rb b/spec/pundit/custom_exception_spec.rb new file mode 100644 index 0000000..5c5d06c --- /dev/null +++ b/spec/pundit/custom_exception_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "spec_helper" + +class User + def policy_class + UserPolicy + end +end + +class UserPolicy + include Pundit::Plus + + class CustomError < Pundit::NotAuthorizedError + def initialize(options = {}) + options[:message] ||= "Custom exception from policy" + super(options) + end + end + + def initialize(user, record) + @user = user + @record = record + end + + def show? + false + end + + def create? + false + end + + def exception_from(query:) + if query == :show? + CustomError + else + super + end + end +end + +RSpec.describe "Pundit.authorize" do + it "raises a custom exception from the policy" do + user = User.new + query = :show? + record = User.new + + expect do + Pundit.authorize(user, record, query) + end.to raise_error(UserPolicy::CustomError).with_message("Custom exception from policy") + end + + it "raises a default exception from the policy" do + user = User.new + query = :create? + record = User.new + + expect do + Pundit.authorize(user, record, query) + end.to raise_error(Pundit::NotAuthorizedError).with_message(/not allowed to create?/) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2cb6f14 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,88 @@ +if ENV["CI"] + require "simplecov" +end +require "pundit/plus" + +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end