Description
I have a working Pundit setup, but I'm now trying to put my pundit policies under the Admin
namespace (config.pundit_policy_namespace = :admin
).
I'm decorating some objects with a decorator class that is also in the Admin
namespace, and this seems to cause authorization to fail.
I suspect that this line in PunditAdapter#namespace
is where things go sour since it's passed an instance of the (namespaced) decorator.
Could a possible fix here be to make sure to undecorate any resource passed to AuthorizationAdapter#initialize
, for example using ResourceController::Decorators.undecorate(resource)?
Expected behavior
When calling PunditAdapter#authorized?
the return value should be based on the policy for the decorated resource.
Actual behavior
PunditAdapter#authorized?
looks for a policy for the decorator class, not finding it, and thus ends up raising an error (or basing the return value on the default policy, if defined).
How to reproduce
I've set up a template below that tests 3 cases:
- Subject decorated with a decorator in the
Admin
namespace ( ❌ pundit can't find policy ) - Subject decorated with a decorator in the top level namespace ( ✔️ succeeds )
- Subject not decorated ( ✔️ succeeds )
Update 2024-01-13
I realized the old template wasn't running properly against latest version of ActiveAdmin, so I added an updated version:
Original version of template
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
# Use local changes or ActiveAdmin master.
if ENV["ACTIVE_ADMIN_PATH"]
gem "activeadmin", path: ENV["ACTIVE_ADMIN_PATH"], require: false
else
gem "activeadmin", github: "activeadmin/activeadmin", require: false
end
# Change Rails version if necessary.
gem "rails", "~> 7.0.0"
gem "sprockets", "~> 3.7"
gem "sassc-rails"
gem "sqlite3", platform: :mri
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "pundit"
# Fixes an issue on CI with default gems when using inline bundle with default
# gems that are already activated
# Ref: rubygems/rubygems#6386
if ENV["CI"]
require "net/protocol"
require "timeout"
gem "net-protocol", Net::Protocol::VERSION
gem "timeout", Timeout::VERSION
end
end
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :active_admin_comments, force: true do |_t|
end
create_table :users, force: true do |t|
t.string :full_name
end
end
require "action_controller/railtie"
require "action_view/railtie"
require "active_admin"
class TestApp < Rails::Application
config.root = __dir__
config.session_store :cookie_store, key: "cookie_store_key"
secrets.secret_token = "secret_token"
secrets.secret_key_base = "secret_key_base"
config.eager_load = false
config.logger = Logger.new($stdout)
config.hosts = "www.example.com"
end
class ApplicationController < ActionController::Base
include Rails.application.routes.url_helpers
end
class User < ActiveRecord::Base
end
module Admin
class UserPresenter
attr_reader :user
delegate_missing_to :user
delegate :to_param, to: :user
def initialize(user)
@user = user
end
end
end
class UserPresenter
attr_reader :user
delegate_missing_to :user
delegate :to_param, to: :user
def initialize(user)
@user = user
end
end
module Admin
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 NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
end
module Admin
class UserPolicy < ApplicationPolicy
def index?
true
end
end
end
ActiveAdmin.setup do |config|
# Authentication disabled by default. Override if necessary.
config.authentication_method = false
config.current_user_method = false
config.authorization_adapter = ActiveAdmin::PunditAdapter
# config.pundit_default_policy = 'Admin::ApplicationPolicy'
config.pundit_policy_namespace = :admin
end
Rails.application.initialize!
Rails.application.routes.draw do
ActiveAdmin.routes(self)
end
require "minitest/autorun"
require "rack/test"
require "rails/test_help"
# Replace this with the code necessary to make your test fail.
class BugTest < ActionDispatch::IntegrationTest
def test_authorization_with_decorator_in_admin_namespace
user = User.create!
subject = Admin::UserPresenter.new(user)
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
def test_authorization_with_decorator_without_namespace
user = User.create!
subject = NonNamespacedUserPresenter.new(user)
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
def test_authorization_with_plain_subject
user = User.create!
subject = user
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
private
def app
Rails.application
end
end
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
# Use local changes or ActiveAdmin master.
if ENV["ACTIVE_ADMIN_PATH"]
gem "activeadmin", path: ENV["ACTIVE_ADMIN_PATH"], require: false
else
gem "activeadmin", github: "activeadmin/activeadmin", require: false
end
#
# Change Rails version if necessary.
gem "rails", "~> 8.0.0"
gem "sprockets", "~> 4.0"
gem "importmap-rails", "~> 2.0"
gem "sqlite3", force_ruby_platform: true, platform: :mri
gem "pundit"
# Fixes an issue on CI with default gems when using inline bundle with default
# gems that are already activated
# Ref: rubygems/rubygems#6386
if ENV["CI"]
require "net/protocol"
require "timeout"
gem "net-protocol", Net::Protocol::VERSION
gem "timeout", Timeout::VERSION
end
end
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new($stdout)
ActiveRecord::Schema.define do
create_table :active_admin_comments, force: true do |_t|
end
create_table :users, force: true do |t|
t.string :full_name
end
end
require "action_controller/railtie"
require "action_view/railtie"
require "active_admin"
class TestApp < Rails::Application
config.root = __dir__
config.hosts << ".example.com"
config.session_store :cookie_store, key: "cookie_store_key"
config.secret_key_base = "secret_key_base"
config.eager_load = false
config.logger = Logger.new($stdout)
Rails.logger = config.logger
end
class ApplicationController < ActionController::Base
include Rails.application.routes.url_helpers
end
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
def self.ransackable_attributes(_auth_object = nil)
authorizable_ransackable_attributes
end
def self.ransackable_associations(_auth_object = nil)
authorizable_ransackable_associations
end
end
class User < ApplicationRecord
end
class UserPresenter
attr_reader :user
delegate_missing_to :user
delegate :to_param, to: :user
def initialize(user)
@user = user
end
def decorated?
true
end
def model
user
end
end
module Admin
class UserPresenter < UserPresenter; end
end
class NonNamespacedUserPresenter < UserPresenter; end
module Admin
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 NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
end
module Admin
class UserPolicy < ApplicationPolicy
def index?
true
end
end
end
ActiveAdmin.setup do |config|
# Authentication disabled by default. Override if necessary.
config.authentication_method = false
config.current_user_method = false
config.authorization_adapter = ActiveAdmin::PunditAdapter
# config.pundit_default_policy = 'Admin::ApplicationPolicy'
config.pundit_policy_namespace = :admin
end
Rails.application.initialize!
Rails.application.routes.draw do
ActiveAdmin.routes(self)
end
require "minitest/autorun"
require "rack/test"
require "rails/test_help"
# Replace this with the code necessary to make your test fail.
class BugTest < ActionDispatch::IntegrationTest
def test_authorization_with_decorator_in_admin_namespace
user = User.create!
subject = Admin::UserPresenter.new(user)
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
def test_authorization_with_decorator_without_namespace
user = User.create!
subject = ::NonNamespacedUserPresenter.new(user)
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
def test_authorization_with_plain_subject
user = User.create!
subject = user
adapter = ActiveAdmin::PunditAdapter.new(subject, nil)
assert adapter.authorized?(:index, subject)
end
private
def app
Rails.application
end
end