I am a new ROR developer. I want to set permission for each type of user with Cancan, from lowest to highest is: guest, member, editor, admin; with higher user has all permissions lower user has. My file ability.rb as below:
include CanCan::Ability
def initialize(user)
unless user
guest_can
else
if user.admin?
admin_can
elsif user.editor?
editor_can(user)
elsif user.member?
member_can(user)
end
end
end
private
def guest_can
can :read, Article
end
def member_can(user)
# member can do whatever guest can
guest_can
can :create, Comment
can [:update, :destroy], Comment, :user_id => user.id
end
def editor_can(user)
# editor can do whatever member can
member_can
can :create, Article
can [:update, :destroy], Article, :user_id => user.id
end
def admin_can
can :manage, :all
end
end
Could you please tell me if my code is good enough or can it cause potential problems? Thank you
Related
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
The following is the condition
I have users who belongs to one work and similarly category belongs to one work.
Now I want the user of that specific work to edit the categories that belongs to that work.
How can I specify this in ability model?
EDIT :
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user
user.roles.each do |role|
role.name
if role.name == "admin"
can :read, User
can :create, User
can :edit, User
can :destroy, User
end
if role.name == "staff"
can :read, :all
can :create, :all
cannot :edit, Category
can :update, :all
can :destroy, :all
end
if role.name == "others"
can :read, :all
end
end
end
end
Untested, but this should do it:
can :manage, Category, :work_id => user.work_id
Reference: https://github.com/ryanb/cancan/wiki/defining-abilities
In the ability class you are passing only the user. You need to pass the category also in order to do,
can :manage, Category if user.work_id == category.work_id
Or you will have to iterate through like this( i ll not recommend it),
can :manage, Category if user.work_id == user.work.categories.first.work_id
This link shows how to send multiple params to your ability class.
https://github.com/ryanb/cancan/wiki/Accessing-Request-Data
Here's my Ability.initialize:
user ||= User.new # handle guest user
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :moderator
can :read, :all
can :update, :all do |clazz|
clazz.managed_by? user
end
elsif user.has_role? :participant
can :update, :all do |clazz|
clazz.owned_by? user
end
can :read, :all do |clazz|
clazz.visible_to? user
end
else
raise "Role decoding in Ability fell through"
end
end
My intention is that the key domain classes [User, Round, Participant, Question and Program] all define an owned_by?, managed_by? and a visible_to? method. And that the rules for allowing one of them to be updated or viewed are applied uniformly.
But I believe that statements like:
can :update, :all do |clazz|
clazz.owned_by? user
end
Are not doing what I think because I don't think I even get to the clazz.owned_by? line.
Can someone straighten me out? I looked at the doc and couldn't really connect what it was saying with the technique I am using. Thank You!
I believe :all is simply a symbol and not an iterator through all classes. You can either list them all explicitly like I'm doing here or use this solution to get a list of all models: https://stackoverflow.com/a/516605/2033014
[User, Round, Participant, Question, Program].each do |klass|
can :update, klass, klass.owned_by?(user)
can :read, klass, klass.visible_to?(user)
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
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