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