My Ability file has
if user.has_role? :admin
can :manage, :all
else
can :manage, Company, :id => Company.with_role(:operator, user).pluck(:id)
...
end
And my Company Controller index has
def index
#companies = Company.with_role(:operator, current_user)
But when I sign in as a User who operates a company, I cannot access that page. (Even though the Company.with_role(:operator, user) returns a relation in the console!)
Companies have Codes. I am not sure how to write this in cancan:
Company.with_role(:operator, user).map{|o| o.codes}
But the wiki says if I use a block then authorize_resource will not set the instance variable #codes, because it doesn't know which objects belong to the user. So I cannot use:
can :manage, Code => do |Code|
user.has_role? :operator, code.company
end
I am looking for a solution that will let my CodesController do:
def index
if params[:company_id]
#keywords = Code.where(:company_id => params[:company_id])
end
And otherwise show the user all their Codes across all of the Companies they have the operator role for.
How about:
can :manage, Company do |company|
user.has_role? :operator, company
end
can :manage, Code do |code|
user.has_role? :operator, code.company
end
If you didn't use block syntax for the can definition you could use load_and_authorize_resource in your CodesController to filter the index to only those that are accessible to the current user.
Update
Because this used the block syntax CanCan can't use determine which objects to load with load_resource (since it wants to use SQL syntax). If you can rewrite it to not use a block syntax then you'll be good. If you have to use the role type logic you can add code similar to the following in your index method in your controller:
#codes = Code.all.select {|code| can?(:manage, code)}
or if you want to bypass the ability in this case for efficiency
#codes = Company.with_role(:operator, #current_user).codes
def index
if params[:company_id]
#codes = Company.with_role(:operator, current_user).where(:id => params[:company_id]).map{|o| o.keywords}.flatten
else
#codes = Company.with_role(:operator, current_user).map{|operator| operator.keywords}.flatten
end
end
Related
Having trouble figuring out how to set up my different roles with cancancan abilities. I have a model "Business" which has many users with a role of either :owner, :manager or :employee.
Im trying to make it first that if they don't belong_to that business they can't see anything for that business. And second I want to limit functionality based on which role they have.
I guess I could do this within the views by using if statements and only showing them the things they have access to, but wondering if there is a better way with cancan
inside your ability.rb
class Ability
include CanCan::Ability
def initialize(user)
alias_action :create, :read, :update, :destroy, :to => :crud
if user
if user.role == "manager"
can :crud, Business, :id => user.business_id
# this will cek whether user can access business instance (id)
elsif user.role == "owner"
can :manage, :all
end
end
end
end
inside your controller you can do checking with 2 ways
step 1: with load_and_authorize_resource, this will automatically check all 7 rails method
class BookingsController < ApplicationController
load_and_authorize_resource
# this before filter will automatically check between users and resource
# rails method here
def show
end
end
step 2: check manually with authorize inside each method
def show
#business = Business.find(params[:id])
authorize! :read, #business
end
Definitely read through cancan's wiki as I'm not 100% on this, but I think the solution will look something like this:
def initialize(user)
user ||= User.new
if user.has_role?(:owner)
can :read, Business, Business.all do |business|
business.id == user.business_id
end
elsif user.has_role?(:whatever)
# etc
end
end
Then just check authorize! in the controller in the normal cancan way. As for showing them appropriate functionality in views, you can either do a bunch of if statements in the view any maybe try to use partials to make it all look palatable, or check in the controller and render different views based on role, but yeah, there's gotta be if statements somewhere.
The best way is to always use "incremental" permissions. Consider that cancancan starts already with the assumption that your users have no right on Business, so you can give them "incremental" permissions based on their role.
An example would be:
# give read access if they have any role related to the business
can :read, Business, users: { id: user.id }
# give write access if they are manager
can [:edit, :update], Business, business_users: { role: 'manager', user: { id: user.id } }
# give destroy permissions to owners
can [:destroy], Business, business_users: { role: 'owner', user: { id: user.id } }
I am a little confused about how to configure CanCanCan properly.
For starters, do I have to add load_and_authorize_resource to every controller resource I want to restrict access to?
This is what I would like to do:
Admin can manage and access all controllers and actions
Editor can read all, manage :newsroom, and can manage all Posts
Member can read every Post and can create & update Posts (not edit/delete/anything else), cannot access the newsroom. The difference between an update & edit post in our business rules is that an update is creating a new post that is a child post of the current post. So it isn't an edit. Just a new record with an ancestry association.
Guest can read every Post, but cannot create Posts nor access the Newsroom.
This is what my ability.rb looks like:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
#Admin
if user.has_role? :admin
can :manage, :all
can :manage, :newsroom
# Editor
elsif user.has_role? :editor
can :read, :all
can :manage, :newsroom
can :manage, Post
#Member
elsif user.has_role? :member
can :read, :all
can :create, Post
can :status, Post
can :update, Post do |post|
post.try(:user) == user
end
#Guest
else
can :read, :all
can :create, Post
can :status, Post
end
end
end
In my routes.rb I have this:
authenticate :user, lambda { |u| u.has_role? :admin or :editor } do
get 'newsroom', to: 'newsroom#index', as: "newsroom"
get 'newsroom/published', to: 'newsroom#published'
get 'newsroom/unpublished', to: 'newsroom#unpublished'
end
What is happening though, is when I am logged in with a user that has not been assigned any roles (i.e. what I want to be a "Guest"), they can access the Newsroom.
When I try to edit a post with the role of :member, it gives me a "Not authorized to edit post" error (which is correct).
I just can't quite lockdown the Newsroom and I am not sure why.
You do not need to use load_and_authorize_resource in every controller. That is a convenience macro that does two things. First it assigns an instance variable with the record(s) assumed for the current controller and action. It then authorizes the resource. For some controller actions the first step might be wrong, so you want to load your resource and then authorize it manually. An example from the Railscasts episode about CanCan is like this:
def edit
#article = Article.find(params[:id])
unauthorized! if cannot? :edit, #article
end
You can also do it like in the example on the CanCan Wiki for authorizing controllers:
def show
#project = Project.find(params[:project])
authorize! :show, #project
end
Or you can just use authorize_resource and take care of loading it yourself. In the end, you must make sure that CanCan is used for authorization somehow (controller macro or in each action). Regarding your abilities, I think you want something like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
#Admin
if user.has_role? :admin
can :manage, :all
# Editor
elsif user.has_role? :editor
can :read, :all
can :manage, :newsroom
can :manage, Post
#Member
elsif user.has_role? :member
can :read, :all
can :create, Post
can :status, Post
can :update, Post do |post|
post.try(:user) == user
end
#Guest
else
can :read, :all
cannot [:index, :published, :unpublished], :newsroom
end
end
end
And here's an example like how you might be able to authorize your newsroom:
class ToolsController < ApplicationController
authorize_resource :class => false
def show
# automatically calls authorize!(:show, :tool)
end
end
A last personal note about CanCan is that I wouldn't suggest it for new projects since it isn't actively maintained anymore and that I found it a bit counterintuitive when defining abilities. That said, CanCan is one of the most well-documented gems that I have worked with, especially the wiki has loads of examples and explanations.
can :read, :all
means user has permission to read all the resources of your app. It should be
can :read, Post
also add
cannot :manage, :newsroom
where you do not want access to newsroom. The order in which you specify permissions matters.
As others have already mentioned, 'load_and_authorize_resource' is optional. Only 'authorize resource' is needed to authorize all actions of a controller. If you skip these then you can 'authorize' individual controller actions.
Avoid using block for ability unless absolutely necessary. For instance if Post has a user_id in it then you could do
can :update, Post, user_id: user.id
Lastly, 'class => false' is used where you do not have a model backing your controller.
i.e you do not have a model called 'Newsroom' but you have a controller called 'NewsroomsController'.
For what it's worth, I had to setup my NewsroomController like this:
class NewsroomController < ApplicationController
authorize_resource :class => false
This is what the working version of my ability.rb looks like after I got it to work with the permissions I needed:
#Roles
#Admin
if user.has_role? :admin
can :manage, :all
# Editor
elsif user.has_role? :editor
can :manage, :newsroom
can :manage, Post
#Member
elsif user.has_role? :member
can [:read, :create, :status], Post
can :update, Post do |post|
post.try(:user) == user
end
#Guest
else
can [:read, :status], Post
end
For starters, do I have to add load_and_authorize_resource to every controller resource I want to restrict access to?
Yes.
What is happening though, is when I am logged in with a user that has
not been assigned any roles (i.e. what I want to be a "Guest"), they
can access the Newsroom.
From the guest role above:
...
#Guest
else
can :read, :all
can :create, Post
can :status, Post
end
This gives a guest read access to everything and the ability to create posts.
If you want your Guests to only be able to read posts it should be:
...
#Guest
else
can :read, Post
# can :status, Post # maybe you want this aswell
end
I am working on a rails application and it requires two different types of roles. One is Employee and other is Admin.
Cancan documentation says that it assumes there is a user or current_user method in the application.
So how can I use cancan to set roles for employee and manager in my app ?
Do like this
write this in application helper
def is_employee?(user)
emp_role = Role.find(:first, :conditions => ["name = ?", "Employee"])
return user.roles.include?(emp_role)
end
def is_admin?(user)
admin_role = Role.find(:first, :conditions => ["name = ?", "Admin"])
return user.roles.include?(admin_role)
end
And abily look like this
class Ability
include CanCan::Ability
include ApplicationHelper
def initialize(user)
# Define abilities for the passed in user here. For example:
#
user ||= Employee.new # guest user (not logged in)
if is_admin?(user)
can :manage, :all
# cannot :manage, IpAddress
elsif is_employee?(user)
#your code
end
For define roles see it
http://stackoverflow.com/questions/10222400/rails-adding-an-admin-role-using-devise-who-can-see-all-the-users/10222813#10222813
It sure works...
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
I've been struggling through this for some time and I've finally got to the point where it seems that CanCan doesn't allow you to authorize a collection of records. For example:
ads_controller.rb
def index
#ads = Ad.where("ads.published_at >= ?", 30.days.ago).order("ads.published_at DESC")
authorize! :read, #ads
end
ability.rb
def initialize(user)
user ||= User.new # Guest user
if user
if user.role? :admin # Logged in as admin
can :manage, :all
else # Logged in as general user
can :read, Ad
can :read_own, Ad, :user_id => user.id
can :create, Ad
end
else # Not logged in (Guest)
can :read, Ad
end
end
This results the unauthorised access message when trying to access the index action.
You are not authorized to access this page.
However, if you change the authorize call in the index action to check on the Ad class rather than the collection like so
def index
#ads = Ad.where("ads.published_at >= ?", 30.days.ago)
authorize! :read, Ad
end
... it works fine.
Any help in explaining this one would be greatly appreciated.
Thanks in advance.
ps. I was originally getting redirect loops when trying to work this out. It turns out there's a gotchya with the recommended rescue_from that you put in the application controller to give you nice error messages. If your root_path is set to the same place where your authorize! call is not true (or failing), you'll get a redirect loop. Comment out the rescue_from Learnt that one the hard way.
CanCan is not designed to be used like that. You can check whether a user has permissions on the model class (e.g. Ad) or a single instance (e.g. #ad).
I suggest you just use accessible_by to filter your collection:
#ads = Ad.where("ads.published_at >= ?", 30.days.ago).accessible_by(current_ability)
# #ads will be empty if none are accessible by current user
raise CanCan::AccessDenied if #ads.empty? # handle however you like
Another approach would be to define a custom permission based on the conditions you use to retrieve the collection:
# ability.rb
can :read_ads_from_past_month, Ad, ["ads.published_at >= ?", 30.days.ago]
# in your controller
def index
authorize! :read_ads_from_past_month, Ad
#ads = Ad.where("ads.published_at >= ?", 30.days.ago)
end
I solved this problem usings splats. In this code example, I am trying to authorize users on a collection of TimeOffRequests. They should be authorized if the User is an admin, a manager or the time off request belongs to them.
# time_off_requests_controller.rb
authorize! :read, *#time_off_requests
# Ability.rb
can :manage, TimeOffRequest do |*time_off_requests|
membership.has_any_role?(:admin, :manager) ||
time_off_requests.all? { |tor| membership.id == tor.employee_id }
end
I wrote about it in detail here if you're interested: http://zacstewart.com/2012/01/08/defining-abilities-for-collections-of-records-using-cancan.html