I'm using Rails 5.2, Pundit 1.1, and rails_admin 1.2
I have the following in my application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized(exception)
flash[:alert] = "You are not authorized to perform this action."
redirect_to root_path
end
end
I have the following in my application.policy.rb:
class ApplicationPolicy
.
.
.
def rails_admin?(action)
case action
when :dashboard
user.admin?
when :index
user.admin?
when :show
user.admin?
when :new
user.admin?
when :edit
user.admin?
when :destroy
user.admin?
when :export
user.admin?
when :history
user.admin?
when :show_in_app
user.admin?
else
raise ::Pundit::NotDefinedError, "unable to find policy #{action} for #{record}."
end
end
end
In my config/initializers/rails_admin.rb I have the following:
RailsAdmin.config do |config|
### Popular gems integration
## == Devise ==
config.authenticate_with do
warden.authenticate! scope: :user
end
config.current_user_method(&:current_user)
## == Pundit ==
config.authorize_with :pundit
end
module RailsAdmin
module Extensions
module Pundit
class AuthorizationAdapter
def authorize(action, abstract_model = nil, model_object = nil)
record = model_object || abstract_model && abstract_model.model
if action && !policy(record).send(*action_for_pundit(action))
raise ::Pundit::NotAuthorizedError.new("not allowed to #{action} this #{record}")
end
#controller.instance_variable_set(:#_pundit_policy_authorized, true)
end
def authorized?(action, abstract_model = nil, model_object = nil)
record = model_object || abstract_model && abstract_model.model
policy(record).send(*action_for_pundit(action)) if action
end
def action_for_pundit(action)
[:rails_admin?, action]
end
end
end
end
end
If a user is an admin then they can access the admin dashboard.
However, if a user isn't an admin then they should be redirected to the home page with a flash message.
When I try to access the Rails Admin dashboard for a non-admin user I get the following:
Why is ApplicationController not rescuing the Pundit::NotAuthorizedError?
I have created a sample app that reproduces this error: https://github.com/helphop/sampleapp
Edit: Update —
The error you are actually having involves path helpers — such as root_url or root_path — inside RailsAdmin. When rescue_from encounters an error, it escapes itself.
With that being said, changing the redirect_to "/" should solve the problem.
You should add config.parent_controller = 'ApplicationController' before config.authorize_with :pundit. It will say Rails Admin that RailsAdmin::ApplicationController inherited from your ApplicationController and rescue will work.
Hope this help.
Related
I use Devise, CanCanCan and rails_admin. With accessibility everything is OK, but I fail to get a flash message and redirect to root_path and getting the screen below. Any ideas, hints on how I could get flash message instead of this? Other flash messages, both alerts and notifications display OK. Thanks a lot.
My ApplicationController is:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :authenticate_user!
def after_sign_in_path_for(resource)
stored_location_for(resource) || welcome_path_url
end
rescue_from CanCan::AccessDenied do |exception|
respond_to do |format|
format.json { head :forbidden, content_type: 'text/html' }
format.html { redirect_to main_app.root_url, notice: exception.message }
format.js { head :forbidden, content_type: 'text/html' }
end
end
end
rails_admin.rb
RailsAdmin.config do |config|
### Popular gems integration
## == Devise ==
config.authenticate_with do
warden.authenticate! scope: :user
end
config.current_user_method(&:current_user)
## == CancanCan ==
# config.authorize_with :cancancan
config.authorize_with do
redirect_to main_app.root_path unless current_user.superuser ==true
end
<....>
end
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
# Define abilities for the passed in user here. For example:
#
user ||= User.new # guest user (not logged in)
if user.superuser?
can :manage, :all
cannot :access, :rails_admin
cannot :manage, :dashboard
end
if user.office?
cannot :access, :rails_admin
cannot :manage, :dashboard
can :read, :all
end
end
end
EDIT:
The working answer is marked.
Also I should define status variable like this:
def status
#user_role ||= User.new(read_attribute(:user_role))
end
in my User model.
It seems RailsAdmin::MainController isn't derived from ApplicationController you defined on the app to catch the exceptions raised on its subclass.
You need to explicitly add the config to use another parent controller like below. Please find the related gem documentation here:
# in config/initializers/rails_admin.rb
config.parent_controller = 'ApplicationController'
This stack overflow answer as well might help you.
I am trying to build a "router" for users that fires on login. I am trying to redirect users by role.
I have the following:
require "#{Rails.root}/lib/client/user_router"
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
add_flash_types :info
def after_sign_in_path_for(user)
UserRouter.new(user)
end
end
and in /lib/user_router.rb
class UserRouter
include Rails.application.routes.url_helpers
def initialize(user)
#user = user
route_user_by_role
end
def route_user_by_role
if #user.is_pt_master?
pt_master_root_url
elsif #user.is_traveler?
traveler_root_url
elsif #user.client_user_role?
route_client_by_role
else
root_url
end
end
def route_client_by_role
if #user.is_super_admin?
for_super_admin
else
for_other_admin
end
end
def for_super_admin
if #user.client_account.blank?
edit_client_account_url
else
client_root_url
end
end
def for_other_admin
if #user.is_first_sign_in? || #user.client_user_info.blank?
edit_client_user_info_url(#user)
else
client_root_url
end
end
end
I am using _url because if I use _path I get a .to_model error, but with _url I am getting Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
I have already set default host in config/environments to be localhost:3000. Any help with how to do this would be greatly appreciated.
Add default_url_options to UserRouter:
class UserRouter
include Rails.application.routes.url_helpers
def self.default_url_options
{host: 'localhost:3000'}
end
def initialize(user)
#user = user
route_user_by_role
end
def route_user_by_role
if #user.is_pt_master?
pt_master_root_url
elsif #user.is_traveler?
traveler_root_url
elsif #user.client_user_role?
route_client_by_role <= breaks here with undefined method error
else
root_url
end
end
def route_client_by_role
if #user.is_super_admin?
for_super_admin
else
for_other_admin
end
end
def for_super_admin
if #user.client_account.blank?
edit_client_account_url
else
client_root_url
end
end
def for_other_admin
if #user.is_first_sign_in? || #user.client_user_info.blank?
edit_client_user_info_url(#user)
else
client_root_url
end
end
end
So I wasn't able to do it as Class in /lib/ like I wanted to, but pulling it out into a module as a Controller concern allowed me to keep the logic separate from the ApplicationController like I wanted to, its not ideal, but better than stuffing it all into App Cntrl.
class ApplicationController < ActionController::Base
include UserRouter
...
def after_sign_in_path_for(user)
initialize_user_router(user)
end
...
end
module UserRouter
def initialize_user_router(user)
#user = user
direct_user_by_role
end
def direct_user_by_role
if #user.is_pt_master?
pt_master_root_path
elsif #user.is_traveler?
direct_traveler
elsif #user.client_user_role?
direct_client_by_role
else
root_path
end
end
def direct_traveler
...
traveler_root_path
end
def direct_client_by_role
if #user.is_super_admin?
direct_super_admins
else
direct_other_admins
end
end
def direct_super_admins
if #user.client_account.blank?
edit_client_account_path
else
client_root_path
end
end
def direct_other_admins
if #user.first_sign_in? || #user.client_user_info.blank?
edit_client_user_info_path(#user)
else
client_root_path
end
end
end
I have two layouts Admin and Domain. And I don't need any extra configuration in Admin layout. but if user tries to access Domain layout they must be in their valid domain.
This means that, I need to customize all of my Domain policy to include both current_user as well as current_domain. I found this can be done with UserContext and pundit_user... so here is what I have done:
application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def pundit_user
UserContext.new(current_user, current_domain)
end
def after_sign_out_path_for(resource)
root_path
end
def current_domain
#current_domain ||= Domain.where(name: requested_domain).first
end
helper_method :current_domain
private
def requested_domain
return request.env["SERVER_NAME"]
end
def user_not_authorized
# reset_session
flash[:alert] = "You are not authorized to perform this action"
redirect_to(request.referrer || root_path)
end
end
Note that, when I access Admin layout, current_domain will be nil and if I visit any routes of Domain layout, then current_domain will set to currently accessing domain.
user_context.rb
class UserContext
attr_reader :current_user, :current_domain
def initialize(current_user, current_domain)
#current_user = current_user
#current_domain = current_domain
end
end
PROBLEM
Suppose I have this policy:
user_policy.rb
class UserPolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def index?
binding.pry # debugging
current_user.admin? ||
current_user.domain == current_domain
end
private
def current_user
# return user.is_a?(User) ? user : user.current_user
user.current_user
end
def current_domain
# return user.is_a?(User) ? nil : user.current_domain
user.current_domain
end
end
when application runs current_user and current_domain must available in UserPolicy as per documentation(https://github.com/elabs/pundit#additional-context).
But I am getting
undefined method `current_user' for #<User:0x007fcefbc2b150>
That means, still I have user object in it, not user.current_user and user.current_domain
Please let me know, if you need further description. What am I missing here?
It was my own dumb mistake.
PROBLEM
I had a before_filter call in domain/base_controller.rb something like:
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# PROBLEM: this line was updating pundit_user again to user object
raise Pundit::NotAuthorizedError unless DomainConsolePolicy.new(current_user, current_domain).authorized?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
SOLUTION:
I have used headless policy for this because now I have both current_user and current_domain set with pundit_user in application_controller
domain/base_controller.rb
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# SOLUTION
authorize :domain_console, :has_access?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
policy/domain_console_policy.rb
class DomainConsolePolicy < Struct.new(:user, :domain_console)
def has_access?
user.current_user.admin? ||
user.current_user.domain_id == user.current_domain.id
end
end
Thanks
I am fairly new to using this Pundit gem but seem to be having trouble understanding the policy system. I have added checks that Pundit has been called for authorization (verify_authorized) and for scoping (verfify_policy_scoped). However, this causes errors when I visit the Devise sessions controller.
Application Controller
class ApplicationController < ActionController::Base
include Pundit
protect_from_forgery
before_filter :authenticate_person!
# Verify that controller actions are authorized. Optional, but good.
after_filter :verify_authorized, except: :index
after_filter :verify_policy_scoped, only: :index
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def pundit_user
Person.find_by_id(current_person)
end
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
# redirect_to(request.referrer || root_path)
end
end
Application Policy
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
#user = user
#record = record
end
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
def scope
Pundit.policy_scope!(user, record.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope
end
end
end
Error Messages
Pundit::AuthorizationNotPerformedError in
Devise::SessionsController#new
Pundit::PolicyScopingNotPerformedError in
...
The answer to this is to check if it's a devise controller.
after_action :verify_authorized, :except => :index, unless: :devise_controller?
From: https://github.com/elabs/pundit/issues/113 and https://gorails.com/forum/using-pundit-with-activeadmin
You probably need to check this section from Pundit's readme.
It basically says, that when using verify_authorized is used in after_action, it will check if authorized was actually called.
Pundit adds a method called verify_authorized to your controllers. This method will raise an exception if authorize has not yet been called. You should run this method in an after_action to ensure that you haven't forgotten to authorize the action.
The same is true for verify_policy_scoped, but for policy_scope:
Likewise, Pundit also adds verify_policy_scoped to your controller. This will raise an exception in the vein of verify_authorized. However, it tracks if policy_scope is used instead of authorize. This is mostly useful for controller actions like index which find collections with a scope and don't authorize individual instances.
In your case exception is caused by the fact that you didn't called authorize in Devise::SessionsController#new action.
I think, the best way to deal with it, is to remove after_action checks from ApplicationController and move them to a subclass.
How do you override the Devise controller to only allow 'admins' to log in?
This is what I came up with:
class SessionsController < Devise::SessionsController
def create
if current_user.admin?
# tell the user "you can't do that"
else
super
end
end
end
but the user was able to log in (probably because 'current_admin' is not defined yet?). Here is the original devise controller action:
class Devise::SessionsController < DeviseController
prepend_before_filter :require_no_authentication, only: [:new, :create]
prepend_before_filter :allow_params_authentication!, only: :create
prepend_before_filter :verify_signed_out_user, only: :destroy
prepend_before_filter only: [:create, :destroy] { request.env["devise.skip_timeout"] = true }
...
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_flashing_format?
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
...
end
Edit: I don't think I should change the session controller, I think I should add a strategy to Warden. I tried this and it still logs in non admin users:
config/initializers/custom_warden_strategies.rb:
Warden::Strategies.add(:admin_only) do
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
encrypted = false
if validate(resource) { encrypted = true; resource.valid_password?(password) }
if resource.admin?
remember_me(resource)
resource.after_database_authentication
success!(resource)
end
end
mapping.to.new.password = password if !encrypted && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
config\initializers\devise.rb
config.warden do |manager|
manager.default_strategies.unshift :admin_only
end
Give this a try:
class SessionsController < Devise::SessionsController
def create
super do
if !resource.admin?
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
set_flash_message :notice, :signed_out if signed_out && is_flashing_format?
respond_to_on_destroy
end
end
end
end
I found a solution, but it fails with my test suite (works when I manually test it though).
config/initializers/admin_only_initializer.rb
require 'devise/strategies/authenticatable'
module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class AdminOnly < Authenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
encrypted = false
if validate(resource){ encrypted = true; resource.valid_password?(password) }
if resource.admin?
success!(resource)
else
fail!(:not_permitted)
end
end
mapping.to.new.password = password if !encrypted && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
end
end
Warden::Strategies.add(:admin_only, Devise::Strategies::AdminOnly)
config/initializers/devise.rb
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :admin_only
end
and the I18n string (config/locales/devise.en.yml):
en:
devise:
failure:
not_permitted: "You are not permitted to complete this action."
Why prevent non-admins from logging in and not just block some actions for non-admin users ? How do you make the difference between an admin and a simple user then ?
Remember, the one true security principle is DENY then ALLOW. So to make sure you application remains safe when you keep adding stuff (AGILE development for example), I suggest the following approach
application_controller.rb
class ApplicationController
before_action :authenticate_user!
# Security policy deny then access
before_filter :access_denied
# Actually I have refactorised below code in a separate security.rb module that I include in ApplicationController, but as you wish
def access_denied
if #denied and not #authorized
flash[:alert] = 'Unauthorized access'
flash[:info] = "Authorized entities : #{#authorized_entities.join(', ')}" if #authorized_entities
flash[:warning] = "Restricted to entities : #{#restricted_entities.join(', ')}" if #restricted_entities
render 'static_pages/home', :status => :unauthorized and return
false
end
end
def allow_access_to_administrators
(#authorized_entities ||= []) << "Administrators"
#authorized = true if administrateur_logged_in?
end
def administrateur_signed_in?
user_signed_in? and current_user.administrator? # Or whatever method you use to authenticate admins
end
end
Note that I use both #authorized and #denied.
I use #authorized generally for a class of users (like admins), whereas I set #denied if, for a class of users, I want to restrict to a subset.
Then I use
your_controller_reserved_for_admins.rb
prepend_before_filter :allow_access_to_administrators