I want to stop users who aren't logged in from accessing the URL using CanCanCan
http://localhost:3000/users
My Ability model is
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
elsif user.roles.size > 0
can :manage, User, :id => user.id
else
can :read, :all
cannot :read, :User
end
end
end
And my Users controller is
class UsersController < ApplicationController
load_and_authorize_resource
def index
#users = User.paginate(page: params[:page],:per_page => 5)
end
def new
# #user = User.new
end
...
end
When I access the page as an guest user. I see the users index page instead of being redirected to login by this code in my application controller
rescue_from CanCan::AccessDenied do |exception|
if user_signed_in?
flash[:error] = "Access denied!"
redirect_to root_url
else
flash[:error] = "Please Sign in"
redirect_to new_user_session_path
end
end
CanCanCan works and stops access to the other actions in the controller just not for index.
In my users controller I was missing
before_filter :authenticate_user!, :except => [:new, :create]
This was allowing the guest user to access the page.
Related
I am currently building a simple web app with Ruby on Rails that allows logged in users to perform CRUD actions to the User model. I would like to add a function where:
Users can select which actions they can perform per controller;
Ex: User A can perform actions a&b in controller A, whereas User B can only perform action B in controller A. These will be editable via the view.
Only authorized users will have access to editing authorization rights of other users. For example, if User A is authorized, then it can change what User B will be able to do, but User B, who is unauthorized, will not be able to change its own, or anyone's performable actions.
I already have my users controller set up with views and a model
class UsersController < ApplicationController
skip_before_action :already_logged_in?
skip_before_action :not_authorized, only: [:index, :show]
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to users_path
else
render :new
end
end
def show
set_user
end
def edit
set_user
end
def update
if set_user.update(user_params)
redirect_to user_path(set_user)
else
render :edit
end
end
def destroy
if current_user.id == set_user.id
set_user.destroy
session[:user_id] = nil
redirect_to root_path
else
set_user.destroy
redirect_to users_path
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
def set_user
#user = User.find(params[:id])
end
end
My sessions controller:
class SessionsController < ApplicationController
skip_before_action :login?, except: [:destroy]
skip_before_action :already_logged_in?, only: [:destroy]
skip_before_action :not_authorized
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to user_path(user.id), notice: 'You are now successfully logged in.'
else
flash.now[:alert] = 'Email or Password is Invalid'
render :new
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: 'You have successfully logged out'
end
end
The login/logout function works, no problem there.
I started off by implementing a not_authorized method in the main application controller which by default prevents users from accessing the respective actions if the user role is not equal to 1.
def not_authorized
return if current_user.nil?
redirect_to users_path, notice: 'Not Authorized' unless current_user.role == 1
end
the problem is that I would like to make this editable. So users with role = 1 are able to edit each user's access authorization, if that makes sense.
How would I go about developing this further? I also do not want to use gems, as the sole purpose of this is for me to learn.
Any insights are appreciated. Thank you!
The basics of an authorization system is an exception class:
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
And a rescue which will catch when your application raises the error:
class ApplicationController < ActionController::Base
rescue_from 'AuthorizationError', with: :deny_access
private
def deny_access
# see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
redirect_to '/somewhere', status: :forbidden
end
end
This avoids repeating the logic all over your controllers while you can still override the deny_access method in subclasses to customize it.
You would then perform authorization checks in your controllers:
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
def create
#thing = current_user.things.new(thing_params)
if #thing.save
redirect_to :thing
else
render :new
end
end
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless #thing.user == current_user || current_user.admin?
end
end
In this pretty typical scenario anybody can create a Thing, but the users can only edit things they have created unless they are admins. "Inlining" everything like this into your controllers can quickly become an unwieldy mess through as the level of complexity grows - which is why gems such as Pundit and CanCanCan extract this out into a separate layer.
Creating a system where the permissions are editable by users of the application is several degrees of magnitude harder to both conceptualize and implement and is really beyond what you should be attempting if you are new to authorization (or Rails). You would need to create a separate table to hold the permissions:
class User < ApplicationRecord
has_many :privileges
end
class Privilege < ApplicationRecord
belongs_to :thing
belongs_to :user
end
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless owner? || admin? || privileged?
end
def owner?
#thing.user == current_user
end
def admin?
current_user.admin?
end
def privileged?
current_user.privileges.where(
thing: #thing,
name: params[:action]
)
end
end
This is really a rudimentary Role-based access control system (RBAC).
I have a jobs model and a link to create a new job. I would like to force a user to sign in before visiting the new job form. I have set up a before_action that forces sign in.
application_controller.rb
helper_method :current_user
helper_method :require_signin!
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def require_signin!
if current_user.nil?
redirect_to signin_path
end
end
jobs_controller.rb
before_action :require_signin!, only: [:new]
routes.rb
get '/auth/twitter' => 'sessions#new', :as => :signin
sessions_controller.rb
class SessionsController < ApplicationController
def create
auth = request.env["omniauth.auth"]
user = User.from_omniauth(auth)
session[:user_id] = user.id
redirect_to user, :notice => "Signed in!"
end
end
Current behavior
When a user, who is not signed in, clicks the 'jobs/new' link, the chain of events is jobs#new -> login -> user (default redirect after signing in). User then must navigate back to jobs#new
Desired behavior
When a user, who is not signed in, clicks the 'jobs/new' link, the chain of events is jobs#new -> login -> jobs#new.
I know the before_action is intercepting the original action, but I would like to complete the original action after signing in. Help?
To implement this, you can save the original route in session before redirecting to sign_in route, like:
class JobsController
before_action :save_original_path, only: [:new]
before_action :require_signin!, only: [:new]
private
def save_original_path
session[:return_to] = new_job_path
end
end
and
class SessionsController
def create
...
redirect_to (session[:return_to] || user), :notice => "Signed in!"
end
end
I'm using Active Admin's CanCan authorization adapter, along with Rolify, to manage authorization on an admin site. I have a model, company, that has_many :manuals, and another model, manuals, that has_many :parts.
If a user does not have access to read admin/manuals/1 and types it into the address bar, they are redirected properly and presented with the unauthorized message. However, if the user types in admin/manuals/1/parts they are not denied access. They are taken to that page, except all the parts are hidden from them. They should be getting redirected to the dashboard with an unauthorized message.
Here is my configuration. Thanks in advance for any advice you can offer.
config/routes.rb
ActiveAdmin.routes(self)
models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :read, ActiveAdmin::Page, :name => "Dashboard"
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :moderator
can :manage, Part, :manual => { :company_id => user.company_id }
else
can :read, Part, :manual => { :company_id => user.company_id }
end
end
end
I've also overwritten the default authorization methods in controllers/application_controller.rb
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
def authenticate_admin_user!
authenticate_user!
unless user_signed_in?
flash[:alert] = "You are not authorized to view this page"
redirect_to root_path
end
end
def current_admin_user #use predefined method name
return nil unless user_signed_in?
current_user
end
def after_sign_in_path_for(user)
if current_user.has_role? :admin
admin_dashboard_path
elsif current_user.has_role? :moderator
admin_manuals_path
else
company_path(user.company)
end
end
Did you add the method load_and_authorize_resource to your controller?
Like this:
class SomeController < ApplicationController
load_and_authorize_resource
...
end
Check Abilities & Authorization
I'd like that after a user logs in, depending on it's role it redirects to some page, I've read this, this, and this and yet I can't seem to do it right, I've tried each one and each has given me issues.
All I want is that after a user logs in, it redirects him/her to x_path, what I have technically works, but after it logs in, it tries to load the redirect_user template.
ApplicationController
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
protect_from_forgery
rescue_from CanCan::AccessDenied do |e|
redirect_to new_user_session_path, alert: e.message
end
def redirect_user
if current_user.role == 'Administrator'
redirect_to rails_admin_path
elsif current_user.role == 'C1' or 'D1' or 'M1' or 'M2'
redirect_to upload_files_path
end
end
end
Routes.rb
Siteconfigurationlistgenerator::Application.routes.draw do
devise_for :users
match '/' => 'application#redirect_user'
Ability
class Ability
include CanCan::Ability
def initialize(user)
#Define abilities for the passed in user here.
user ||= User.new #guest user (not logged in)
#a signed-in user can do everything
if user.role == 'Administrator'
#an admin can do everything
can :manage, :all
can :access, :rails_admin # grant access to rails_admin
can :dashboard # grant access to the dashboard
elsif user.role == '1' or 'M1' or 'D1' or 'M2'
can :manage, [MaterialList, Inventory, UploadFiles, DownloadTemplate]
cannot :access, :rails_admin
cannot :dashboard
can :access, [:upload_files, :download_template]
end
end
end
Currently, as I have it, it says something like template not found, it looks up the redirect_user template, in the views/application folder and it shouldn't, it should just redirect
Your routes.rb should have:
devise_for :users, :controllers => { :sessions => "sessions" }
You then need to create a sessions_controller.rb:
class SessionsController < Devise::SessionsController
protected
def after_sign_in_path_for(resource)
if resource.role == 'Administrator'
rails_admin_path
elsif resource.role == 'C1' or 'D1' or 'M1' or 'M2'
upload_files_path
else
super
end
end
end
You will notice that this method returns the path, it does not call redirect_to I have also added a call to super so that the default behaviour is called if none of the conditions match.
I am using authlogic and cancan on a rails 3 application, I want to allow all logged in users to access the users index page, i have tried something like this but it dosent seem to be working:
ability class:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :index, User if UserSession.find
can :read, User if UserSession.find
end
Controller:
def index
#users = User.search(params[:search]).order('username').page(params[:page]).per(1)
authorize! :index, #users
end
def show
#user = User.find(params[:id])
authorize! :read, #user
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #user }
end
end
thanks
I find it's easier to use load_and_authorize_resource at the top of my controllers. Then your ability class contains all the ability logic instead of having it strewn about your controllers.
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user
can :index, User
can [:show, :edit, :update, :destroy], User, :id => user.id
end
end
end
users_controller.rb
class UsersController < ApplicationController
load_and_authorize_resource
def index
#users = User.search(params[:search]).order('username').page(params[:page]).per(1)
end
def show
end
...
end
I haven't used authlogic in a while as I tend to use devise now, so I'm not sure if my sample code is authlogic ready. If you don't want to use load_and_authorize_resource, my code shows how to limit what users can see in the ability class, but in your code I'd change :read to :show.
Continuing from my comment, the problem was in the following code
authorize! :index, #users
Here, you're passing an Array of users to the CanCan's method, while your can :index, User declaration defines the authorization for a User object.