My question is absolutely theoretic, like "Is it right thing to do?".
I'm new to Rails in particular and to Ruby in general, and I'm trying to use Cancan autorization solution for my Rails appilcation.
Let's consider we have a simple contoller like this, a pair of associated views and an User model with DB table.
class UsersController < ApplicationController
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
end
The goal is to restrict access to the "index" method to all but admins and permit regular users to see only their own pages, e.g. to permit user with id==5 to see page "users/5".
For this scope I've create an ability class for Cancan. Here it is:
class Ability
include CanCan::Ability
def initialize user, options = {}
default_rules
if user
admin_rules(user) if user.role.eql? "admin"
player_rules(user) if user.role.eql? "player"
end
end
def admin_rules user
can :read, UsersController
end
def player_rules user
can :read, User do |user_requested|
user_requested.id == user.id
end
end
def default_rules
end
end
My question is that:
Should I use UsersController as an object in "can" method if I do not have a handy one of type User? To applicate it later by "authorize! :show, UsersController" in the "index" method of the controller. Or it should be done in some other way?
Thank you for your suggestions.
No you don't want to add the UsersController to CanCan.
CanCan is meant to authorize resources, not Rails Controllers.
I would suggest the following:
def initialize(user)
if user.is_admin?
can :manage, User
else
can :manage, User, :id => user.id
end
end
This would allow the user only access to his own user unless he is an admin.
See the Defining abilities page in CanCan Wiki
I use a symbol, e.g., in the Ability class
def initialize(user)
if user.is_admin?
can :any, :admin
end
end
and in the controller
authorize! :any, :admin
In the wiki I found another way to set the ability. It's kind of advanced though, check it out here.
ApplicationController.subclasses.each do |controller|
if controller.respond_to?(:permission)
clazz, description = controller.permission
write_permission(clazz, "manage", description, "All operations")
controller.action_methods.each do |action|
...
+1 to #Tigraine.
Follow his instructions...
class Ability
include CanCan::Ability
def initialize user, options = {}
default_rules
if user
admin_rules(user) if user.role.eql? "admin"
player_rules(user) if user.role.eql? "player"
end
end
def admin_rules user
can :manage, User
end
def player_rules user
can :manage, User :id => user.id
end
def default_rules
end
end
and do this in your controller...
class UsersController < ApplicationController
load_and_authorize_resource
# => #users for index
# => #user for show
def index
end
def show
end
end
for details on load_and_authorize_resource see the bottom of this link
Related
I've a controller without model. In this controller we are loading some other resources. Most importantly the application has multi-tenancy feature.
Here is the code:
# ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.owner?
can :manage, Tool, tenant_id: user.tenant.id
end
end
end
# boxes_controller.rb
class BoxesController < ApplicationController
authorize_resource class: false
def index
tools = Tool.all
end
end
What is the problem?:
Say, user1 of tenant1 creates tool1 and the user2 of tenant2 creates tool2.
The problem is, from tenant1, user1 can access tool2! :(
Did I wrote anything wrong? Please help.
Try this
if user.owner?
can :manage, Tool do |tools|
tools.tenant_id == user.tenant.id
end
end
and in controller
authorize_resource class: false
Hope that helps!
It is really bad habit of mine "not to read documentation of a gem completely". I've found solution here: https://github.com/ryanb/cancan/wiki/Fetching-Records
# ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.owner?
can :manage, Tool, tenant_id: user.tenant.id
end
end
end
# boxes_controller.rb
class BoxesController < ApplicationController
authorize_resource class: false
before_action :set_tool, only: %i(edit update destroy)
def index
tools = Tool.accessible_by(current_ability)
end
private
def set_tool
#tool = Tool.find params[:id]
authorize! :manage, #tool
end
end
Maybe it would help someone. Thanks!
This is what my ability.rb looks like:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
end
can :manage, Connection, inviter_user_id: user.id
end
end
In my controller I have this:
class ConnectionsController < ApplicationController
load_and_authorize_resource
skip_authorize_resource only: :index
layout 'connections'
def index
#family_tree = current_user.family_tree
#inviter_connections = current_user.inviter_connections.order("updated_at desc")
#invited_connections = current_user.invited_connections.order("updated_at desc")
end
end
In my application_controller.rb, I have this:
rescue_from CanCan::AccessDenied do |exception|
redirect_to authenticated_root_url, :alert => exception.message
end
Yet, when I try to visit /connections when I am not logged in, I get this error:
NoMethodError at /connections
undefined method `family_tree' for nil:NilClass
Also, when I remove the can :manage, Connection from my ability.rb it actually sends me to my login page like I expect.
How do I get both to work?
It looks like you are using Devise for authentication. For this kind of validation when using devise you should add this to your controller:
before_action :authenticate_user!
Try the following:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
else
can :manage, Connection, inviter_user_id: user.id
end
end
end
Also, noticed that you are skip_authorize_resource only: :index, try commenting out that and see if it works.
On line 8 of your controller, current_user is nil when you're not logged in, and it's calling family_tree on it.
You need something like (just as an example, it depends on your needs):
#family_tree = current_user.try(:family_tree) || FamilyTree.new
The reason it "works" when you remove the line in Ability is because that removes the ability to see the connection, so the before_filter redirects before you ever get inside index. What's probably tripping you up is the Connection record has a inviter_user_id of nil, and User#id is nil, so it's giving you permission to get into index.
It could also happen if you forgot to put this at the top of the controller:
load_and_authorize_resource
See docs for more.
I want to "map" MyController to User model
#ability
class Ability
include CanCan::Ability
def initialize user
user ||= User.new
if user.has_role? :admin
can :manage, :all
else
can :read, :all
can :read, User do |u|
u && u.user_id == user.id
end
end
end
end
#routes
get 'my_controller/show/(:id)', to: 'MyController#show'
#controller
class MyController < ApplicationController
# load_and_authorize_resource
def show
# showing a user
end
end
So my_controller/show/(:id) can be accessible only for admin and current user. For example, the user with id == 2 can see the their own profile my_controller/show/2 and cannot others users' ones such as my_controller/show/1234 unless they are an admin.
There is no My model in the the project. Ideally, I have to rename MyController to UserController but I'm not allowed to do that for some reason.
If I uncomment load_and_authorize_resource at MyController, there will be an error of
NameError in MyController#show
uninitialized constant My
So how can I get rid of it?
You can specify the name of the type of resource to load and authorize:
load_and_authorize_resource :user
Otherwise cancan will attempt to work it out by convention (based on the name of the controller).
https://github.com/ryanb/cancan/wiki/authorizing-controller-actions
I have a controller with a method like;
def show
if params[:format].eql?("pdf")
// do something
elsif params[:format].eql?("csv")
// do something
end
end
But i have users with different roles. So i use CanCan to manage access control.
Now i want X role can do the action show in controller iff params[:format].eql?("csv")
I think it can be like ;can :show, resource if params[:format].eql?("csv"). So how can i send parameters to ability.rb?
Any idea?
Thanks.
In ApplicationController add the following:
# CanCan - pass params in to Ability
# https://github.com/ryanb/cancan/issues/133
def current_ability
#current_ability ||= Ability.new(current_user, params)
end
The most current answer is in the CanCan wiki: https://github.com/ryanb/cancan/wiki/Accessing-Request-Data
can takes two arguments: first is type of action that user is trying to perform on a resource, second is resource (can be class name or instance variable) itself. If you have your Ability set correctly, you should be able to do something like this:
def show
if params[:format].eql?("pdf")
// do something
elsif params[:format].eql?("csv")
if can? :read, resource
#do stuff
end
end
end
Don't forget that you have to have your user authenticated before running any CanCan checks.
can? method only returns true or false. I normally like to use authorize! method to check abilities. Unlike can, it would rise CanCan::AccessDenied error that you can rescue and process gracefully. Something in the lines of:
#models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role? :admin
can :manage, :all
elsif user.role? :hiring_manager
can [:read, :update], Post, user_id: user.id
end
end
end
#controllers/posts_controller.rb
class PostsController < ApplicationController::Base
before_filter :authenticate_user
def show
#post = Post.find(params[:id])
authorize! :read, #post # will thorow an exception if not allowed
end
end
Then, I just catch the exception on ApplicationController level.
I have an app that uses Devise and CanCan.
in the config>initializers>Abiliity.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.is? :superadmin
can :manage, :all
elsif user.is? :user
can :read, Project do |project|
project && project.users.include?(user)
end
end
end
end
I have problem with the index action of Project controller, the project controller is a normal stock RESTful controller. Basically, a user who's a normal user, when logged in, can see the projects#index. But not all projects have this user as 'normal user', why isn't cancan blocking his access?
Thanks
Make sure you're calling load_and_authorize_resource in your ProjectsController, along the lines of:
class ProjectsController < ApplicationController
load_and_authorize_resource
#...
end
If that still doesn't work, try calling the authorize! method inside the index action, to see if that makes a difference, eg:
class ProjectsController < ApplicationController
#...
def index
#projects = Project.all
authorize! :read, #projects
end
#...
end