Cancan not showing authorized view elements
-
26-09-2019 - |
Question
I'm trying to get some basic authentication/authorization with devise/cancan with Rails. Rather than using roles like Ryan B's screencast and other examples around I'm trying to do something basic:
1 - A user can log in
2 - A user can only edit/destroy their own articles (no roles, you're either logged in and can create new articles and edit/destroy your own or you're logged out and you can only see articles and login)
I'm using devise for the first part and that's working well but I can't get the second part working with CanCan. The the edit and destroy links for the articles don't appear when you're logged in and the direct URL (e.g. /articles/3/edit) still allows even if the article is for another user.
My ability.rb
is
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user
if user.nil?
can :read, :all
else
# can :manage, :all #test - with this, all the edit/destroy links appear
can :manage, Article, :user_id == user
end
end
end
articles_controller.rb
:
class ArticlesController < ApplicationController
before_filter :authenticate_user!, :except => [:index, :show] # for Devise
load_and_authorize_resource
# GET /articles
# GET /articles.xml
def index
@articles = Article.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @articles }
end
end
# GET /articles/1
# GET /articles/1.xml
def show
@article = Article.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @article }
end
end
# GET /articles/new
# GET /articles/new.xml
def new
@article = Article.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @article }
end
end
# GET /articles/1/edit
def edit
@article = Article.find(params[:id])
end
# POST /articles
# POST /articles.xml
def create
@article = Article.new(params[:article])
@article.user = current_user
respond_to do |format|
if @article.save
format.html { redirect_to(articles_path, :notice => 'Article was successfully created.') }
format.xml { render :xml => articles_path, :status => :created, :location => articles_path }
else
format.html { render :action => "new" }
format.xml { render :xml => @article.errors, :status => :unprocessable_entity }
end
end
end
# PUT /articles/1
# PUT /articles/1.xml
def update
@article = Article.find(params[:id])
respond_to do |format|
if @article.update_attributes(params[:article])
format.html { redirect_to(@article, :notice => 'Article was successfully updated.') }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @article.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /articles/1
# DELETE /articles/1.xml
def destroy
@article = Article.find(params[:id])
@article.destroy
respond_to do |format|
format.html { redirect_to(articles_url) }
format.xml { head :ok }
end
end
end
and the view partial that lists articles _article_list.html.erb
:
<table>
<tr>
<th>Title</th>
<th>Description</th>
<th>User</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.description %></td>
<td><%= article.user_id %></td>
<td><%= link_to 'Show', article %></td>
<% if can? :update, @article %>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<% end %>
<% if can? :destroy, @article %>
<td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
<% end%>
</tr>
<% end %>
</table>
With this setup, the edit/destroy links in the view don't show up unless there's a blanket can :manage, :all
, even can :manage, Article
doesn't work. As I mentioned above, it also isn't restricting the actual actions as you're able to deep link straight to editing an article and it permits it.
I'm not sure what I'm doing wrong here. It would be great to get some help.
Thanks in advance
Jason
Solution
I managed to resolve my problem. I reset my environment (rvm - resintalled the gems and gemsets - ruby 1.9.2 and rails 3.0.0) and changed some of the code and all the issues I was having went away (redirect loop, view elements not changing based on being logged in, unauthorized controller actions still permissable). I've pasted ability.rb
, articles_controller.rb
, and _article_list.html.erb
.
ability.rb
:
class Ability
include CanCan::Ability
def initialize(user)
if user
can :create, Article
can :read, :all
can :update, Article, :user_id => user.id
can :delete, Article, :user_id => user.id
else
can :read, :all
end
end
end
I guess it makes sense now but because only update and delete were supposed to be for the current user's articles, I split out the CRUD elements to be specific.
articles_controller.rb
class ArticlesController < ApplicationController
before_filter :authenticate_user!, :except => [:index, :show]
# load_and_authorize_resource # RESTful automated CanCam authorization - excludes non RESTful
# GET /articles
# GET /articles.xml
def index
@articles = Article.all
authorize! :read, @articles
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @articles }
end
end
# GET /articles/1
# GET /articles/1.xml
def show
@article = Article.find(params[:id])
authorize! :read, @article
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @article }
end
end
# GET /articles/new
# GET /articles/new.xml
def new
@article = Article.new
authorize! :create, @article
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @article }
end
end
# GET /articles/1/edit
def edit
@article = Article.find(params[:id])
authorize! :update, @article
end
# POST /articles
# POST /articles.xml
def create
@article = Article.new(params[:article])
@article.user = current_user
authorize! :create, @article
respond_to do |format|
if @article.save
format.html { redirect_to(articles_path, :notice => 'Article was successfully created.') }
format.xml { render :xml => articles_path, :status => :created, :location => articles_path }
else
format.html { render :action => "new" }
format.xml { render :xml => @article.errors, :status => :unprocessable_entity }
end
end
end
# PUT /articles/1
# PUT /articles/1.xml
def update
@article = Article.find(params[:id])
authorize! :update, @article
respond_to do |format|
if @article.update_attributes(params[:article])
format.html { redirect_to(@article, :notice => 'Article was successfully updated.') }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @article.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /articles/1
# DELETE /articles/1.xml
def destroy
@article = Article.find(params[:id])
@article.destroy
authorize! :delete, @article
respond_to do |format|
format.html { redirect_to(articles_url) }
format.xml { head :ok }
end
end
def by
@user = User.find(params[:id])
@articles = @user.articles
authorize! :read, @articles
end
end
load_and_authorize_resource
works but I've put specific authorize! lines in each controller action as I have an extra action at the bottom. Both now work.
I updated the reference to @article to article to reference the current article in the list in _article_list.html.rb
:
<table>
<tr>
<th>Title</th>
<th>Description</th>
<th>User</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.description %></td>
<td><%= article.user_id %></td>
<td><%= link_to 'Show', article %></td>
<% if can? :update, article %>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<% end %>
<% if can? :delete, article %>
<td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
<% end %>
</tr>
<% end %>
</table>
All working now. Thanks for the help here and hopefully this will help someone else out if they run into this problem.
OTHER TIPS
Your condition for matching a user id isn't quite right. It should be:
can :manage, Article, :user_id => user.id
The attribute you want to check is mapped to the value you want to check against.
Also, you are checking for user.nil?
when it can't be nil because you've just initialised it. (Probably a symptom of having tried lots of things!)
Does your catch work? If you uncomment the can :manage, :all line will a user be able to edit his/ her post ( along with everyone else's of course )?
Have you tried changing, can :manage, Article, :user_id == user to
can :manage, Article do |article|
article.try(:user) == user
I have never been able to get load an authorize to work- although I suspect that I was doing something wrong. To prevent someone from accessing the url directly, in your article's edit action, try adding this
unauthorized! if cannot? :edit, @article