From f7192940eadb18d07c94b543c8f8a7c23dfc87b0 Mon Sep 17 00:00:00 2001 From: Andrei Makarov Date: Mon, 28 Apr 2025 15:45:40 +0300 Subject: [PATCH 1/2] Introduce ActiveAdmin::ActionPolicyAdapter References: - https://gist.github.com/amkisko/c704c1a6462d573dfa4820ae07d807a6 - actionpolicy.evilmartians.io --- lib/active_admin/action_policy_adapter.rb | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 lib/active_admin/action_policy_adapter.rb diff --git a/lib/active_admin/action_policy_adapter.rb b/lib/active_admin/action_policy_adapter.rb new file mode 100644 index 00000000000..fd186373d87 --- /dev/null +++ b/lib/active_admin/action_policy_adapter.rb @@ -0,0 +1,44 @@ +module ActiveAdmin + class ActionPolicyAdapter < AuthorizationAdapter + def authorized?(action, subject = nil) + target = policy_target(subject) + policy = ActionPolicy.lookup(target) + action = format_action(action, subject) + policy.new(target, user: user).apply(action) + end + + def scope_collection(collection, _action = Auth::READ) + target = policy_target(collection) + policy = ActionPolicy.lookup(target) + policy.new(user: user).apply_scope(collection, type: :active_admin) + end + + def format_action(action, subject) + case action + when Auth::CREATE + :create? + when Auth::UPDATE + :update? + when Auth::READ + subject.is_a?(Class) ? :index? : :show? + when Auth::DESTROY + subject.is_a?(Class) ? :destroy_all? : :destroy? + else + "#{action}?" + end + end + + private + + def policy_target(subject) + case subject + when nil + resource.resource_class + when Class + subject.new + else + subject + end + end + end +end From 27197895f8cbf57f98c472044fbd91daf9c76611 Mon Sep 17 00:00:00 2001 From: Andrei Makarov Date: Fri, 2 May 2025 12:31:36 +0300 Subject: [PATCH 2/2] Add ActionPolicy support This commit introduces the ActionPolicy adapter for ActiveAdmin, allowing for flexible authorization management. The ActionPolicy gem is added to the Gemfile, and relevant documentation is updated to guide users on configuring the adapter. Additionally, new policy templates are included to demonstrate usage. Ref: https://actionpolicy.evilmartians.io --- Gemfile | 1 + Gemfile.lock | 4 + docs/13-authorization-adapter.md | 60 ++++++++++++++ features/authorization_action_policy.feature | 47 +++++++++++ gemfiles/rails_70/Gemfile | 1 + gemfiles/rails_70/Gemfile.lock | 11 +-- gemfiles/rails_71/Gemfile | 1 + gemfiles/rails_71/Gemfile.lock | 11 +-- gemfiles/rails_72/Gemfile | 1 + gemfiles/rails_72/Gemfile.lock | 11 +-- lib/active_admin.rb | 1 + lib/active_admin/action_policy_adapter.rb | 83 ++++++++++++++----- .../install/templates/active_admin.rb.erb | 16 +++- .../active_admin/comment_policy.rb | 9 ++ .../action_policy/active_admin/page_policy.rb | 12 +++ .../action_policy/active_admin/user_policy.rb | 19 +++++ .../action_policy/admin_user_policy.rb | 4 + .../action_policy/application_policy.rb | 67 +++++++++++++++ .../policies/action_policy/category_policy.rb | 4 + .../policies/action_policy/company_policy.rb | 4 + .../policies/action_policy/post_policy.rb | 9 ++ .../policies/action_policy/store_policy.rb | 4 + .../policies/action_policy/tag_policy.rb | 5 ++ .../policies/action_policy/user_policy.rb | 4 + spec/unit/action_policy_adapter_spec.rb | 74 +++++++++++++++++ 25 files changed, 419 insertions(+), 44 deletions(-) create mode 100644 features/authorization_action_policy.feature create mode 100644 spec/support/templates/policies/action_policy/active_admin/comment_policy.rb create mode 100644 spec/support/templates/policies/action_policy/active_admin/page_policy.rb create mode 100644 spec/support/templates/policies/action_policy/active_admin/user_policy.rb create mode 100644 spec/support/templates/policies/action_policy/admin_user_policy.rb create mode 100644 spec/support/templates/policies/action_policy/application_policy.rb create mode 100644 spec/support/templates/policies/action_policy/category_policy.rb create mode 100644 spec/support/templates/policies/action_policy/company_policy.rb create mode 100644 spec/support/templates/policies/action_policy/post_policy.rb create mode 100644 spec/support/templates/policies/action_policy/store_policy.rb create mode 100644 spec/support/templates/policies/action_policy/tag_policy.rb create mode 100644 spec/support/templates/policies/action_policy/user_policy.rb create mode 100644 spec/unit/action_policy_adapter_spec.rb 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 index fd186373d87..ca84d000f28 100644 --- a/lib/active_admin/action_policy_adapter.rb +++ b/lib/active_admin/action_policy_adapter.rb @@ -1,44 +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) - target = policy_target(subject) - policy = ActionPolicy.lookup(target) action = format_action(action, subject) - policy.new(target, user: user).apply(action) + retrieve_policy(subject).apply(action) end def scope_collection(collection, _action = Auth::READ) - target = policy_target(collection) - policy = ActionPolicy.lookup(target) - policy.new(user: user).apply_scope(collection, type: :active_admin) + 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::CREATE - :create? - when Auth::UPDATE - :update? - when Auth::READ - subject.is_a?(Class) ? :index? : :show? - when Auth::DESTROY - subject.is_a?(Class) ? :destroy_all? : :destroy? - else - "#{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 - resource.resource_class - when Class - subject.new - else - 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