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
Related
I have the following namespaces ApiController
class Api::ApiController < ApplicationController
skip_before_action :verify_authenticity_token,
if: Proc.new { |c| c.request.content_type == 'application/json' }
before_action :authenticate
attr_reader :current_user
private
def authenticate
#current_user = AuthorizeApiRequest.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless #current_user
end
end
On AuthorizeApiRequest.call, Rails complains that:
uninitialized constant Api::ApiController::AuthorizeApiRequest
My AuthorizeApiRequest class is defined under app/commands:
class AuthorizeApiRequest
prepend SimpleCommand
def initialize(headers = {})
#headers = headers
end
def call
user
end
private
attr_reader :headers
def user
#user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
#user || errors.add(:token, 'Invalid token') && nil
end
def decoded_auth_token
#decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
nil
end
end
So it seems to not allow me to call AuthorizeApiRequest.call without added namespace to front. How to fix?
Your app/commands folder doesn't seem to be loaded into Rails at boot.
You need to include your app/commands in your autoload paths for this to work or require the file manually in your controller.
See: https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths
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.
To have a cleaner code I want to split my controller in some concerns.
In my routes.rb how to redirect to concern without redefine the methods of concern index show destroy create ...
class SomeController
include SomeConcern
def index
end
end
module SomeConcern
def index
end
end
Sorry for my bad english.
Lets say we have a CarsController and AirplanesController that have the typical create and new actions.
class AirplanesController < ApplicationController
def new
#airplane = Airplane.new
end
def create
#airplane = Airplane.new(create_params)
if #airplane.save
redirect_to #airplane
else
render :new
end
end
# ...
end
class CarsController < ApplicationController
def new
#car = Car.new
end
def create
#car = Car.new(create_params)
if #car.save
redirect_to #car
else
render :new
end
end
# ...
end
To dry this up we can extract the shared code to a module:
module Createable
extend ActiveSupport::Concern
included do
attr_accessor :resource
alias_attribute :self.controller_name.to_sym, :resource
end
def new
#resource = resource_class.new
yield #resource if block_given?
end
def create
#resource = resource_class.new(create_params)
if #resource.save
yield #resource if block_given?
redirect_to #resource
else
render :new
end
end
private
def create_params
raise "not implemented controller!"
end
def resource_class
#resource_class ||= self.controller_name.classify.constantize
end
end
We can then apply it to the controller classes by:
class CarsController < ApplicationController
include Createable
def create_params
params.require(:car)
.permit(:model) # ...
end
end
class AirplanesController < ApplicationController
include Createable
def create_params
params.require(:airplane)
.permit(:model) # ...
end
end
But a very important point here is that you are not routing to the module. The module is providing methods to the controller class.
You have to always map to your controller. Concerns are modules where you can put shared logic (it makes sense only in case you need 2 absolutely similar methods in 2 different controllers).
I think, that such code should work:
class SomeController
include SomeConcern
end
module SomeConcern
def index
end
end
Isn't it?
But concerns mostly used to move out some private helper methods from controller, rather actions as we do in this code piece
The policy_scope works perfectly finding the correct policy named Admin::RemittancePolicy but authorize method not.
module Admin
class RemittancesController < AdminController # :nodoc:
...
def index
#remittances = policy_scope(Remittance).all
render json: #remittances
end
def show
authorize #remittance
render json: #remittance
end
...
end
end
Take a look at output error:
"#<Pundit::NotDefinedError: unable to find scope `RemittancePolicy::Scope` for `Remittance(...)`>"
Perhaps a error with pundit, I really not know how fix it. Thanks.
More information below:
# policies/admin/admin_policy.rb
module Admin
class AdminPolicy < ApplicationPolicy # :nodoc:
def initialize(user, record)
#user = user
#record = record.is_a?(Array) ? record.last : record
end
def scope
Pundit.policy_scope! user, record.class
end
class Scope # :nodoc:
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope.is_a?(Array) ? scope.last : scope
end
def resolve
scope
end
end
end
end
# controllers/admin/admin_controller.rb
module Admin
class AdminController < ActionController::API # :nodoc:
include Knock::Authenticable
include Pundit
before_action :authenticate_user
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
# def policy_scope!(user, scope)
# model = scope.is_a?(Array) ? scope.last : scope
# PolicyFinder.new(scope).scope!.new(user, model).resolve
# end
def policy_scope(scope)
super [:admin, scope]
end
def authorize(record, query = nil)
super [:admin, record], query
end
end
end
Your stacktrace says the error comes from
app/policies/admin/admin_policy.rb:9:in 'scope'
That's this:
def scope
Pundit.policy_scope! user, record.class
end
record.class evaluates to Remittance, so if I understand what you're trying to do, you need to change scope to
def scope
Pundit.policy_scope! user, [:admin, record.class]
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