I'm making a rails project, and I'm trying to implement CanCanCan. I installed the gem and the ran the commands. I then added this to ability.rb:
class Ability
include CanCan::Ability
def initialize(user)
# Define abilities for the passed in user here. For example:
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
else
can :update, Post do |post|
post.user == user
end
can :destroy, Post do |post|
post.user == user
end
can :create, Post
can :read, :all
end
end
end
However, now in my project, if I sign into a different user, I can still edit other users posts.
Any help with what I'm missing will be greatly appreciated.
The error was solved by adding load_and_authorize_resource to my posts_controller
According to CanCanCan documentation,
The block is only evaluated when an actual instance object is present.
It is not evaluated when checking permissions on the class (such as in
the index action). This means any conditions which are not dependent
on the object attributes should be moved outside of the block.
don't do this
can :update, Project do |project|
user.admin? # this won't always get called
end
do this
can :update, Project if user.admin?
So instead of
can :update, Post do |post|
post.user == user
end
can :destroy, Post do |post|
post.user == user
end
Change that do
can :update, Post if post.user == user
can :destroy, Post if post.user == user
Try something like can :manage, Post, :user_id => user.id
As you can see at https://github.com/ryanb/cancan/wiki/defining-abilities
I have it set up the same way as nersoh, and that method works for me. Have you tried authorizing the Posts resource in the controller? For example,
class PostsController < ActionController::Base
load_and_authorize_resource
end
Related
I'm implementing CanCanCan for the first time.
But am confused why users can still create posts when I've setup cannot :manage, Post in the Ability class.
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # if a non-signedin visitor
cannot :manage, Post
end
end
My understanding is that :manage applies to all actions, so the user should not be able to do anything with the post resource.
Can anyone advise?
have you added this around the index, show and edit pages CRUD links across your app? that will remove the link all together.. Im still new with cancancan but even with the load_and_authorize_resources I still had to add the if can? around my links and that resolved my problem.
<% if can? :manage, Post %>
<% link_to "something" some_path %>
<% end %>
hope this helps
First, you need to define the abilities of each user type inside the Ability class:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # if a non-signedin visitor
# abilities of user of type PUBLISHER
if user.role == "publisher"
can :create, Post
can :read, Post
can :update, Post, user_id: user.id # users can only update their own posts
# abilities of user of type ADMIN
elsif user.role == "admin"
can :manage, :all # admin can do everything
# abilities of non-users (e.g. they only can read posts)
else
cannot :manage, Post
can :read, Post
end
end
Defining the abilities does not mean they are automatically
applicable in controllers.
Second, you need to include load_and_authorize_resource in each controller you want to apply the defined abilities to (e.g. the Post controller).
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 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
Here is a snippet of my code from my ability class
if user.admin?
can :manage, :all
can :destroy, :all if != current_user
I am sure that you can figure out what I am trying to do here. I realize that destroy is included in manage and I am repeating myself there. Any suggestions?
EDIT Yjerem's answer was the correct one and I just changed it to fit my code. This is what it looks like.
if user.admin?
can :manage, :all
cannot :destroy, User, :id => user.id
As Yjerem also said, in cancan, ability precedence states that the ability defined lower down trump the ones over them so an admin can manage all except what is defined under it using the code above.
Read Ability Precedence, there's an example there just for you!
Basically what you want is the cannot method:
if user.admin?
can :manage, :all
cannot :destroy, User, :id => current_user.id
Because the cannot rule is below the more general one, it overrides it.
I would try something like this (assuming you have an Account/User model):
def initialize(user)
...
if user.admin?
can :manage, :all
can :destroy, Account do |account|
account.user != user # admin can destroy all Accounts/Users except his own
end
end
...
end
I'm running into problems defining user permissions in my cancan controller:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user
if user.role? :admin
can :manage, :all
else
can :read, :all
can :update, User do |user|
user.try(:user) == user
end
end
end
end
This results in a NoMethodError:
undefined method `user' for #<User:0x000001050914c8>
When I try and edit / update a user. Everything else seems just fine.
Any help appreciated
Bob
The problem is that
user.try(:user) == user
is basically trying to execute user.user == user
Looks like you're trying to only let users update the User model attributes if the User instance in question is the logged-in user.
Try this instead:
can :update, User, :id => user.id
Which is saying "Can update the User model when #user.id is the same as the current_user.id."
Your block notation is ambiguous since your block variable |user| is the same as the user passed in to the Ability model.
As a side-note for those still getting a grip on Ruby,
can :update, User, :id => user.id
is the same as:
can(:update, User, { id: user.id })