I am using the CanCan Gem and would like to assign a User two roles which conflict. I need a User to have two roles, one is the x role and the other is the y role. Role x allows users to create posts but not create articles. Role y allows users to create articles but not create posts.
My ability.rb file is like the following:
if user.role?(x)
can :manage, Post
cannot :manage, Article
end
if user.role?(y)
cannot :manage, Post
can :manage, Article
end
Currently if I assign a User both roles, the permissions for role Y will override the permissions for role x. I would like to allow Admins to stack roles so that if I want to add roles x and y to a User, that User will be able to manage Posts and Articles. So if the role has a can and another role has a cannot, the can permission overrides.
Shouldn't
if user.role?(x)
can :manage, Post
end
if user.role?(y)
can :manage, Article
end
be enough?
cannot just removes previously granted permissions. But if you do not grant them, then you do not need to remove them.
See also https://github.com/CanCanCommunity/cancancan/wiki/Ability-Precedence
Here is a running example:
require 'cancan'
class Ability
include CanCan::Ability
attr_reader :user
def initialize(user)
#user = user
if user.role?(:editor)
can :edit, Post
end
if user.role?(:reader)
can :read, Post
end
end
end
class Post
end
class User
attr_reader :name
def initialize(name, *roles)
#name = name
#roles = roles
end
def role?(role)
#roles.include?(role)
end
end
admin = User.new('admin', :reader, :editor)
user = User.new('user', :reader)
editor = User.new('editor', :editor)
admin_ability = Ability.new(admin)
user_ability = Ability.new(user)
editor_ability = Ability.new(editor)
def output(ability, permission)
puts "#{ability.user.name} can #{permission} Post: #{ability.can?(permission, Post)}"
end
output(admin_ability, :edit)
output(user_ability, :edit)
output(editor_ability, :edit)
puts "--"
output(admin_ability, :read)
output(user_ability, :read)
output(editor_ability, :read)
The default is to NOT HAVE a permission. So if you only work in "additive" mode where you add permissions if a user has roles, you will never need to remove one of them.
:manage is the same as [:create, :edit, :update, :new, :destroy, :index, :show] and is only there for convenience. If you only want to add 6 of those permissions, you can add all 7 and then remove 1. But other than that I don't think you'll need cannot for your example.
The accepted answer is now 5 years old, and this functionality appears to be in CanCanCan as expected by the OP.
It did not work as expected in version 1.15, but does work in version 3.3, so you may need to upgrade your cancancan gem.
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 trying to use cancancan for authorization. When I am an authorized 'group_creator' user I am still denied access to groups/new.
Roles are functional and has_role? from rolify works. So I think the issue comes with cancancan or maybe devise.
Ability.rb
def initialize(user)
user ||= User.new # guest user (not logged in)
alias_action :create, :read, :update, :destroy, to: :crud
if user.has_role? :group_creator
can :create, Group
elsif user.has_role?(:creator, Group)
can :manage, Group, owner_id: user.id
elsif user.has_role?(:admin, Group)
can :crud, Group, :id => Group.with_role(:admin, user).pluck(:id)
else
can :read, Group
end
end
Relevant GroupsController.rb
class GroupsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource
def index
#groups = Group.all # Can be deleted due to cancancan?
end
def new
#group = Group.new # Can also be deleted due to cancancan...
end
end
I have tried debugging in the console with:
user=User.last
user.has_role? :group_creator # returns true
group=Group.last
ability=Ability.new(user)
ability.can?(:create, Group) # returns false
ability.can?(:create, group) # returns false
Two things here.
The user needs :read AND :create ability to access new action. This solved this problem for me.
After fiddling with a fresh mind this morning some advice for others running across this that are new like me... Make sure to reboot server after altering ability.rb... (I am not sure if this was part of the issue yesterday but I thought I should mention it). Cancancan wiki has some explanation on how to utilize a separate permissions table for dynamic modification of abilities.
I'm working on my abilities model on my Rails app to define my user authorization/abilities using cancancan. At the moment, here's what my ability.rb file looks like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
else
can :manage, Application
can :read, User
end
end
end
I'm testing my app now. when I log into an account that doesn't have the admin boolean and go to to say /users, they can access that page, but clicking the show buttons or anything else leads them to the root page and says that they don't have permission. Currently I have an Applications controller/model (poor naming convention, realized later). But I simply wanted to make it so that any user who has a boolean FALSE for if they're admin has different abilities. I want the admin? true person to be able to do anything, but I want everyone else to be only able to SEE THEIR OWN own application (not everything entered by all users), only be able to create one application, and only be able to see their own information on the user show page, and only be able to edit their own user information. Can anyone explain how to specify for the things only the users create and such? Thank you!
define role column into your database and insert following line into your model
generate migration:
class AddRoleToUser < ActiveRecord::Migration
def change
add_column :users, :role, :integer
end
end
insert this into your model like (User.rb)
enum role: [:Admin, :Client, :Enduser, :ClientUser, :Moderator]
then you can manage your permission using cancancan
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
else
can [:new,:create, :update, :destroy], User
end
end
end
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 utilizing Devise and Cancan for a rails 3.2.6 application. In the application, I allow users to create a document with some information gathered in a form. I then want to allow the user to list on a Document index page at localhost:3000/users/1/documents only their documents, which this is working. What isn't working, is I am trying to limit the user from being able to see everyone else's documents by replacing the /users/:id/documents with another number.
I am using cancan and have tried both
can :index, Document, :user_id => user.id
can :read, Document, :user_id => user.id
and then on the Document controller index method
if can? :read, Document
#documents = #user.documents
else
redirect_to root_path
end
also tried with :index as well...but this isn't working. I am also using load_and_authorize_resource..
Any thoughts on what I am missing?
I will say, cancan is working for my user management and users controller for an admin to create, list and edit users, so I know cancan is working in general. It is also working for updating and deleting a users documents. It is just the index function not working.
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.id
if user.has_role? :user
can :create, Document
can :read, Document, :user_id => user.id
can :update, Document, :user_id => user.id
end
end
end
end
You have to make sure that non-logged in users, as well as users whose user.id isn't the same as the Document's user_id (document owner) don't have permission to read all Documents.
class Ability
include CanCan::Ability
def initialize(account)
user ||= User.new #non-logged-in user
# logged in users
if user.id and user.has_role?(:user)
#content owners
can :manage, Document, :user_id => user.id
#other logged-in users
can [:show, :create], Document
end
end
end
Be careful you don't have any line like can :read, :all or can :read, Document most likely you are giving the permission somewhere if you said cancan is working already.
In your case you should write in your ability class
def initialize(user)
can :manage, Document do |document|
document.user == user
end
end
This will check whether document belongs to logged in user or not. If yes can will return true otherwise false.
For more details on how to handle complex authorization with block,
https://github.com/ryanb/cancan/wiki/Defining-Abilities-with-Blocks