diff --git a/Gemfile b/Gemfile index 2ee6627530e..da7f501ed1c 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ group :development, :test do gem "cancancan" gem "pundit" + gem "action_policy" gem "draper" gem "devise" diff --git a/Gemfile.lock b/Gemfile.lock index da0e1d251b3..19621350902 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,8 @@ PATH GEM remote: https://rubygems.org/ specs: + action_policy (0.7.4) + ruby-next-core (>= 1.0) actioncable (8.0.2) actionpack (= 8.0.2) activesupport (= 8.0.2) @@ -397,6 +399,7 @@ GEM rubocop-rspec (3.6.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) + ruby-next-core (1.1.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) securerandom (0.4.1) @@ -453,6 +456,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + action_policy activeadmin! cancancan capybara diff --git a/docs/13-authorization-adapter.md b/docs/13-authorization-adapter.md index 326cb0a128f..2f7ed774867 100644 --- a/docs/13-authorization-adapter.md +++ b/docs/13-authorization-adapter.md @@ -283,3 +283,63 @@ in your application instead of default one generated by Pundit's In addition, there are [example policies](https://github.com/activeadmin/activeadmin/tree/master/spec/support/templates/policies/active_admin) for restricting access to ActiveAdmin's pages and comments. + +## Using the ActionPolicy Adapter + +Active Admin also provides an adapter out of the box for [ActionPolicy](https://github.com/palkan/action_policy). + +To use the ActionPolicy adapter, update the configuration in the Active Admin +initializer: + +```ruby +config.authorization_adapter = ActiveAdmin::ActionPolicyAdapter +``` + +Once that's done, Active Admin will pick up your ActionPolicy policies, and use +them for authorization. For more information about setting up ActionPolicy, see +[their documentation](https://actionpolicy.evilmartians.io/#/). + +You can specify a default policy class that will be used when ActionPolicy is unable +to find a suitable policy: + +```ruby +config.action_policy_default_policy = MyDefaultPolicy +``` + +If you wish to maintain a separate set of ActionPolicy policies for admin +resources, you may set a namespace here that ActionPolicy will search +within when looking for a resource's policy: + +```ruby +config.action_policy_namespace = ActiveAdmin +``` + +Example policy with namespace and scope type: + +```ruby +module ActiveAdmin + class UserPolicy < ApplicationPolicy + scope_for(:active_admin) do |relation| + user.administrator? ? relation : relation.none + end + + def index? = user.administrator? + + def show? = user.administrator? + + def create? = user.administrator? + + def update? = user.administrator? + + def destroy? = user.administrator? + end +end +``` + +If you want to use batch actions, ensure that `destroy_all?` method is defined +in your policy class. You can use this [template +policy](https://github.com/activeadmin/activeadmin/blob/master/spec/support/templates/policies/action_policy/application_policy.rb) +in your application. + +In addition, there are [example policies](https://github.com/activeadmin/activeadmin/tree/master/spec/support/templates/policies/action_policy/active_admin) +for restricting access to ActiveAdmin's pages and comments. diff --git a/features/authorization_action_policy.feature b/features/authorization_action_policy.feature new file mode 100644 index 00000000000..63fb1100b28 --- /dev/null +++ b/features/authorization_action_policy.feature @@ -0,0 +1,47 @@ +@authorization +Feature: Authorizing Access using ActionPolicy + + Background: + Given I am logged in + And 1 post exists + And a configuration of: + """ + require 'action_policy' + + ActiveAdmin.application.namespace(:admin).authorization_adapter = ActiveAdmin::ActionPolicyAdapter + ActiveAdmin.application.namespace(:admin).action_policy_namespace = :action_policy + + ActiveAdmin.register Post do + end + + ActiveAdmin.register_page "No Access" do + end + """ + And I am on the index page for posts + + Scenario: Attempt to access a resource I am not authorized to see + When I go to the last post's edit page + Then I should see "You are not authorized to perform this action" + + Scenario: Viewing the default action items + When I follow "View" + Then I should not see an action item link to "Edit" + + Scenario: Attempting to visit a Page without authorization + When I go to the admin no access page + Then I should see "You are not authorized to perform this action" + + Scenario: Viewing a page with authorization + When I go to the admin dashboard page + Then I should see "Dashboard" + + Scenario: Comment policy allows access to my own comments only + Given 5 comments added by admin with an email "commenter@example.com" + And 3 comments added by admin with an email "admin@example.com" + When I am on the dashboard + Then I should see a menu item for "Comments" + When I go to the index page for comments + Then I should see 3 Comments in the table + When I go to the last post's show page + Then I should see 3 comments + And I should be able to add a comment \ No newline at end of file diff --git a/gemfiles/rails_70/Gemfile b/gemfiles/rails_70/Gemfile index e40f0dda1b6..cccf89df1d1 100644 --- a/gemfiles/rails_70/Gemfile +++ b/gemfiles/rails_70/Gemfile @@ -6,6 +6,7 @@ group :development, :test do gem "cancancan" gem "pundit" + gem "action_policy" gem "draper" gem "devise" diff --git a/gemfiles/rails_70/Gemfile.lock b/gemfiles/rails_70/Gemfile.lock index 63ce6f544c6..db9f4a19260 100644 --- a/gemfiles/rails_70/Gemfile.lock +++ b/gemfiles/rails_70/Gemfile.lock @@ -14,6 +14,8 @@ PATH GEM remote: https://rubygems.org/ specs: + action_policy (0.7.4) + ruby-next-core (>= 1.0) actioncable (7.0.8.7) actionpack (= 7.0.8.7) activesupport (= 7.0.8.7) @@ -236,7 +238,6 @@ GEM matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.8) minitest (5.25.5) multi_test (1.1.0) net-imap (0.5.7) @@ -249,9 +250,6 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) @@ -333,6 +331,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) + ruby-next-core (1.1.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) simplecov (0.22.0) @@ -352,8 +351,6 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.7.3) - mini_portile2 (~> 2.8.0) sqlite3 (1.7.3-arm64-darwin) sqlite3 (1.7.3-x86_64-linux) sys-uname (1.3.1) @@ -380,10 +377,10 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-linux DEPENDENCIES + action_policy activeadmin! cancancan capybara diff --git a/gemfiles/rails_71/Gemfile b/gemfiles/rails_71/Gemfile index 8e94220e958..245cddc85ec 100644 --- a/gemfiles/rails_71/Gemfile +++ b/gemfiles/rails_71/Gemfile @@ -6,6 +6,7 @@ group :development, :test do gem "cancancan" gem "pundit" + gem "action_policy" gem "draper" gem "devise" diff --git a/gemfiles/rails_71/Gemfile.lock b/gemfiles/rails_71/Gemfile.lock index 72219d289be..f0f6d71aaa6 100644 --- a/gemfiles/rails_71/Gemfile.lock +++ b/gemfiles/rails_71/Gemfile.lock @@ -14,6 +14,8 @@ PATH GEM remote: https://rubygems.org/ specs: + action_policy (0.7.4) + ruby-next-core (>= 1.0) actioncable (7.1.5.1) actionpack (= 7.1.5.1) activesupport (= 7.1.5.1) @@ -254,7 +256,6 @@ GEM marcel (1.0.4) matrix (0.4.2) mini_mime (1.1.5) - mini_portile2 (2.8.8) minitest (5.25.5) multi_test (1.1.0) mutex_m (0.3.0) @@ -268,9 +269,6 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) @@ -366,6 +364,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) + ruby-next-core (1.1.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) securerandom (0.4.1) @@ -386,8 +385,6 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (2.6.0) - mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-arm64-darwin) sqlite3 (2.6.0-x86_64-linux-gnu) stringio (3.1.7) @@ -415,10 +412,10 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-linux DEPENDENCIES + action_policy activeadmin! cancancan capybara diff --git a/gemfiles/rails_72/Gemfile b/gemfiles/rails_72/Gemfile index 242a1c316a1..586555ae1cf 100644 --- a/gemfiles/rails_72/Gemfile +++ b/gemfiles/rails_72/Gemfile @@ -6,6 +6,7 @@ group :development, :test do gem "cancancan" gem "pundit" + gem "action_policy" gem "draper" gem "devise" diff --git a/gemfiles/rails_72/Gemfile.lock b/gemfiles/rails_72/Gemfile.lock index d15e2ce8ac1..e5ac7e90924 100644 --- a/gemfiles/rails_72/Gemfile.lock +++ b/gemfiles/rails_72/Gemfile.lock @@ -14,6 +14,8 @@ PATH GEM remote: https://rubygems.org/ specs: + action_policy (0.7.4) + ruby-next-core (>= 1.0) actioncable (7.2.2.1) actionpack (= 7.2.2.1) activesupport (= 7.2.2.1) @@ -248,7 +250,6 @@ GEM marcel (1.0.4) matrix (0.4.2) mini_mime (1.1.5) - mini_portile2 (2.8.8) minitest (5.25.5) multi_test (1.1.0) net-imap (0.5.7) @@ -261,9 +262,6 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) @@ -359,6 +357,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) + ruby-next-core (1.1.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) securerandom (0.4.1) @@ -379,8 +378,6 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (2.6.0) - mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-arm64-darwin) sqlite3 (2.6.0-x86_64-linux-gnu) stringio (3.1.7) @@ -409,10 +406,10 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-linux DEPENDENCIES + action_policy activeadmin! cancancan capybara diff --git a/lib/active_admin.rb b/lib/active_admin.rb index 148f34468e2..227067a4d20 100644 --- a/lib/active_admin.rb +++ b/lib/active_admin.rb @@ -24,6 +24,7 @@ module ActiveAdmin autoload :Callbacks, "active_admin/callbacks" autoload :Component, "active_admin/component" autoload :CanCanAdapter, "active_admin/cancan_adapter" + autoload :ActionPolicyAdapter, "active_admin/action_policy_adapter" autoload :ControllerAction, "active_admin/controller_action" autoload :CSVBuilder, "active_admin/csv_builder" autoload :Dependency, "active_admin/dependency" diff --git a/lib/active_admin/action_policy_adapter.rb b/lib/active_admin/action_policy_adapter.rb new file mode 100644 index 00000000000..ca84d000f28 --- /dev/null +++ b/lib/active_admin/action_policy_adapter.rb @@ -0,0 +1,83 @@ +ActiveAdmin::Dependency.action_policy! + +require "action_policy" + +# Add a setting to the application to configure the action policy default policy +ActiveAdmin::Application.inheritable_setting :action_policy_default_policy, nil +ActiveAdmin::Application.inheritable_setting :action_policy_namespace, nil +ActiveAdmin::Application.inheritable_setting :action_policy_scope_type, :active_admin + +module ActiveAdmin + class ActionPolicyAdapter < AuthorizationAdapter + def authorized?(action, subject = nil) + action = format_action(action, subject) + retrieve_policy(subject).apply(action) + end + + def scope_collection(collection, _action = Auth::READ) + retrieve_policy(collection).apply_scope(collection, type: default_scope_type) + end + + def retrieve_policy(subject) + target = policy_target(subject) + @policies ||= {} + @policies[target] ||= ActionPolicy.lookup(target, namespace: default_policy_module) + @policies[target].new(target, user: user) + rescue ActionPolicy::NotFound + if default_policy_class + default_policy(subject) + else + raise ActionPolicy::NotFound, "unable to find a compatible policy for `#{target}`" + end + end + + def format_action(action, subject) + case action + when Auth::READ then subject.is_a?(Class) ? :index? : :show? + when Auth::DESTROY then subject.is_a?(Class) ? :destroy_all? : :destroy? + else "#{action}?" + end + end + + private + + def default_policy + ActiveAdmin.application.action_policy_default_policy + end + + def default_policy_class + return if default_policy.nil? + + default_policy.to_s.camelize.constantize + end + + def default_policy_namespace + ActiveAdmin.application.action_policy_namespace + end + + def default_policy_module + return if default_policy_namespace.nil? + + default_policy_namespace.to_s.camelize.constantize + end + + def default_scope_type + ActiveAdmin.application.action_policy_scope_type&.to_sym + end + + def policy_target(subject) + case subject + when nil then resource.resource_class + when Class then subject.new + else subject + end + end + + def apply_namespace(subject) + return subject unless default_policy_namespace + return subject if subject.class.to_s.start_with?("#{default_policy_module}::") + + [default_policy_namespace.to_sym, subject] + end + end +end diff --git a/lib/generators/active_admin/install/templates/active_admin.rb.erb b/lib/generators/active_admin/install/templates/active_admin.rb.erb index 1432cfc4dbe..358bfd1d543 100644 --- a/lib/generators/active_admin/install/templates/active_admin.rb.erb +++ b/lib/generators/active_admin/install/templates/active_admin.rb.erb @@ -68,10 +68,11 @@ ActiveAdmin.setup do |config| # Active Admin will automatically call an authorization # method in a before filter of all controller actions to # ensure that there is a user with proper rights. You can use - # CanCanAdapter, PunditAdapter, or make your own. Please + # CanCanAdapter, PunditAdapter, ActionPolicyAdapter, or make your own. Please # refer to the documentation. # config.authorization_adapter = ActiveAdmin::CanCanAdapter # config.authorization_adapter = ActiveAdmin::PunditAdapter + # config.authorization_adapter = ActiveAdmin::ActionPolicyAdapter # In case you prefer Pundit over other solutions you can here pass # the name of default policy class. This policy will be used in every @@ -83,6 +84,19 @@ ActiveAdmin.setup do |config| # within when looking for a resource's policy. # config.pundit_policy_namespace = :admin + # In case you prefer ActionPolicy over other solutions you can here pass + # the name of default policy class. This policy will be used in every + # case when ActionPolicy is unable to find suitable policy. + # config.action_policy_default_policy = "ActiveAdminDefaultPolicy" + + # If you wish to maintain a separate set of ActionPolicy policies for admin + # resources, you may set a namespace here that ActionPolicy will search + # within when looking for a resource's policy. + # config.action_policy_namespace = "ActiveAdmin" + + # If you wish to use a different scope type for ActionPolicy, you can set it here. + # config.action_policy_scope_type = :active_admin + # You can customize your CanCan Ability class name here. # config.cancan_ability_class = "Ability" diff --git a/spec/support/templates/policies/action_policy/active_admin/comment_policy.rb b/spec/support/templates/policies/action_policy/active_admin/comment_policy.rb new file mode 100644 index 00000000000..30657a04d00 --- /dev/null +++ b/spec/support/templates/policies/action_policy/active_admin/comment_policy.rb @@ -0,0 +1,9 @@ +module ActionPolicy::ActiveAdmin + class CommentPolicy < ActionPolicy::ApplicationPolicy + scope_for(:active_admin) do |relation| + relation.where(author: user) + end + + def destroy? = author? || administrator? + end +end diff --git a/spec/support/templates/policies/action_policy/active_admin/page_policy.rb b/spec/support/templates/policies/action_policy/active_admin/page_policy.rb new file mode 100644 index 00000000000..30591e05152 --- /dev/null +++ b/spec/support/templates/policies/action_policy/active_admin/page_policy.rb @@ -0,0 +1,12 @@ +module ActionPolicy::ActiveAdmin + class PagePolicy < ActionPolicy::ApplicationPolicy + def show? + case record.name + when "Dashboard" + true + else + false + end + end + end +end diff --git a/spec/support/templates/policies/action_policy/active_admin/user_policy.rb b/spec/support/templates/policies/action_policy/active_admin/user_policy.rb new file mode 100644 index 00000000000..178d16b8081 --- /dev/null +++ b/spec/support/templates/policies/action_policy/active_admin/user_policy.rb @@ -0,0 +1,19 @@ +module ActionPolicy::ActiveAdmin + class UserPolicy < ActionPolicy::ApplicationPolicy + scope_for(:active_admin) do |relation| + administrator? ? relation : relation.none + end + + def index? = administrator? + + def show? = administrator? + + def create? = administrator? + + def update? = administrator? + + def destroy? = administrator? + + def destroy_all? = administrator? + end +end diff --git a/spec/support/templates/policies/action_policy/admin_user_policy.rb b/spec/support/templates/policies/action_policy/admin_user_policy.rb new file mode 100644 index 00000000000..020b45636fc --- /dev/null +++ b/spec/support/templates/policies/action_policy/admin_user_policy.rb @@ -0,0 +1,4 @@ +module ActionPolicy + class AdminUserPolicy < ActionPolicy::ApplicationPolicy + end +end diff --git a/spec/support/templates/policies/action_policy/application_policy.rb b/spec/support/templates/policies/action_policy/application_policy.rb new file mode 100644 index 00000000000..2182c07226c --- /dev/null +++ b/spec/support/templates/policies/action_policy/application_policy.rb @@ -0,0 +1,67 @@ +class ActionPolicy::ApplicationPolicy < ActionPolicy::Base + def initialize(record = nil, user:) + super + @user = user + @record = record + end + + def index? + false + end + + def show? + false + end + + def new? + create? + end + + def create? + false + end + + def edit? + update? + end + + def update? + false + end + + def destroy? + false + end + + def destroy_all? + false + end + + private + + def authenticated? + return false if user.blank? + + true + end + + def not_authenticated? + !authenticated? + end + + def author? + authenticated? && record.try(:author) == user + end + + def owner? + authenticated? && record.try(:owner) == user + end + + def user_record? + authenticated? && record == user + end + + def administrator? + authenticated? && user.try(:administrator?) + end +end diff --git a/spec/support/templates/policies/action_policy/category_policy.rb b/spec/support/templates/policies/action_policy/category_policy.rb new file mode 100644 index 00000000000..17df702131d --- /dev/null +++ b/spec/support/templates/policies/action_policy/category_policy.rb @@ -0,0 +1,4 @@ +module ActionPolicy + class CategoryPolicy < ActionPolicy::ApplicationPolicy + end +end diff --git a/spec/support/templates/policies/action_policy/company_policy.rb b/spec/support/templates/policies/action_policy/company_policy.rb new file mode 100644 index 00000000000..567a4eddb50 --- /dev/null +++ b/spec/support/templates/policies/action_policy/company_policy.rb @@ -0,0 +1,4 @@ +module ActionPolicy + class CompanyPolicy < ActionPolicy::ApplicationPolicy + end +end diff --git a/spec/support/templates/policies/action_policy/post_policy.rb b/spec/support/templates/policies/action_policy/post_policy.rb new file mode 100644 index 00000000000..a877de0cd53 --- /dev/null +++ b/spec/support/templates/policies/action_policy/post_policy.rb @@ -0,0 +1,9 @@ +module ActionPolicy + class PostPolicy < ActionPolicy::ApplicationPolicy + def new? = true + + def create? = record.category.nil? || record.category.name != "Announcements" || user.is_a?(User::VIP) + + def update? = author? + end +end diff --git a/spec/support/templates/policies/action_policy/store_policy.rb b/spec/support/templates/policies/action_policy/store_policy.rb new file mode 100644 index 00000000000..112605e2709 --- /dev/null +++ b/spec/support/templates/policies/action_policy/store_policy.rb @@ -0,0 +1,4 @@ +module ActionPolicy + class StorePolicy < ActionPolicy::ApplicationPolicy + end +end diff --git a/spec/support/templates/policies/action_policy/tag_policy.rb b/spec/support/templates/policies/action_policy/tag_policy.rb new file mode 100644 index 00000000000..d12cb518a50 --- /dev/null +++ b/spec/support/templates/policies/action_policy/tag_policy.rb @@ -0,0 +1,5 @@ +module ActionPolicy + class TagPolicy < ActionPolicy::ApplicationPolicy + end +end + diff --git a/spec/support/templates/policies/action_policy/user_policy.rb b/spec/support/templates/policies/action_policy/user_policy.rb new file mode 100644 index 00000000000..e381b44057d --- /dev/null +++ b/spec/support/templates/policies/action_policy/user_policy.rb @@ -0,0 +1,4 @@ +module ActionPolicy + class UserPolicy < ActionPolicy::ApplicationPolicy + end +end diff --git a/spec/unit/action_policy_adapter_spec.rb b/spec/unit/action_policy_adapter_spec.rb new file mode 100644 index 00000000000..ad9815b2a8b --- /dev/null +++ b/spec/unit/action_policy_adapter_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::ActionPolicyAdapter do + describe "full integration" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(application, "Admin") } + let(:resource) { namespace.register(Post) } + let(:user) { User.new } + let(:auth) { namespace.authorization_adapter.new(resource, user) } + + before do + namespace.authorization_adapter = ActiveAdmin::ActionPolicyAdapter + end + + it "should initialize the policy stored in the namespace configuration" do + expect(auth.authorized?(:read, Post)).to eq true + expect(auth.authorized?(:update, Post)).to eq false + end + + it "should treat :new ability the same as :create" do + expect(auth.authorized?(:new, Post)).to eq true + expect(auth.authorized?(:create, Post)).to eq true + end + + it "should scope the collection" do + collection = double + expect(collection).to receive(:accessible_by).with(auth.action_policy, :read) + auth.scope_collection(collection, :read) + end + + context "when ActionPolicy namespace provided" do + before do + allow(ActiveAdmin.application).to receive(:action_policy_namespace).and_return :foobar + end + + it "looks for a namespaced policy" do + expect(ActionPolicy).to receive(:lookup).with(Post).and_return(DefaultPolicy) + auth.authorized?(:read, Post) + end + + it "looks for a namespaced policy scope" do + collection = double + expect(ActionPolicy).to receive(:lookup).with(collection).and_return(DefaultPolicy) + auth.scope_collection(collection, :read) + end + + it "uses the resource when no subject given" do + expect(ActionPolicy).to receive(:lookup).with(resource).and_return(DefaultPolicy) + auth.authorized?(:index) + end + end + + context "when ActionPolicy is unable to find policy" do + let(:record) { double } + + subject(:policy) { auth.retrieve_policy(record) } + + before do + allow(ActiveAdmin.application).to receive(:action_policy_default_policy).and_return "DefaultPolicy" + end + + it("should return default policy instance") { is_expected.to be_instance_of(DefaultPolicy) } + + context "and default policy doesn't exist" do + let(:default_policy_klass_name) { nil } + + it "raises the error" do + expect { subject }.to raise_error ActionPolicy::NotFound + end + end + end + end +end \ No newline at end of file