Skip to content

Instantly share code, notes, and snippets.

@steveklabnik
Created December 12, 2011 13:04
Embed
What would you like to do?
Wtf cancan?
# Article is a normal AR class, no methods, just attributes. free is a boolean attribute.
#
# /articles/1 is free, /articles/2 is not.
#
# I want only logged in users to read non-free articles. But the :free => true line seems to be enabling
# reading all of them; when I comment it out, non-logged-in users can't read anything. But with it
# uncommented, they can read everything. WTF?
#
# User.new.is? :user is false, and so is User.new.is? :admin.
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.is? :user
can :show, Article
elsif user.is? :admin
can [:show, :create, :update], Article
else
can :show, Article, :free => true
cannot :show, Article, :free => false
end
end
end
class ArticleRepository
class << self
delegate :find, :new, :to => Article
def root
Article.first
end
end
end
class ArticlesController < ApplicationController
def new
@article = ArticleRepository.new
end
def create
@article = ArticleRepository.new params[:article]
@article.update_attributes(params[:article])
@article.save
redirect_to article_path(@article)
end
def show
#begin
@article = ArticleRepository.find(params[:id])
authorize!(:show, @article)
#rescue ActiveRecord::RecordNotFound
# render :create
# return
#end
end
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
include RoleModel
roles :admin, :user
end
@steveklabnik
Copy link
Author

@spoonito the try method does not work. And Article.find(1).free #=> true and Article.find(2).free #=> false, so...

@therabidbanana
Copy link

Have you tried reducing it down to just the non-user case to ensure the free bit is actually the issue?


  def initialize(user)
      user ||= User.new
      can :show, Article, :free => true
      cannot :show, Article, :free => false
  end

@therabidbanana
Copy link

Seems User.new.is? :user would always be true. Or am I missing something?

@steveklabnik
Copy link
Author

@therabidbanana that code also allows you to see all Articles, logged in and not.

@steveklabnik
Copy link
Author

ruby-1.9.3-p0 :001 > User.new.is? :user
 => false 

I've added my User class to the gist.

@spoonito
Copy link

You can show where you fetch articles? or where you use the permission?

@steveklabnik
Copy link
Author

@spoonito updated. It's pretty basic.

@mattdenner
Copy link

Have you tried reversing logic? So define paid to be !free and then adjusting the CanCan rules. Maybe :free as an option means something to CanCan. (he says, completely guessing)

@spoonito
Copy link

Try on ability.rb put can :show, ArticleRepository, :free => true

@divout
Copy link

divout commented Dec 12, 2011

can :read instead of :show?

@steveklabnik
Copy link
Author

@divout read means :show and :index, and I have no index.

@spoonito, I think you're on to something...

@mattdenner, didnt' help :/

@steveklabnik
Copy link
Author

@spoonito, even changing it to just Article.find in the show action doesn't do anything.

So strange. It's like it ignores my condition. I can either view both or neither.

@spoonito
Copy link

@steveklabnik cancan authorize_method:

def authorize_resource
unless skip?(:authorize)
@controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
end
end

I suppose that:
authorization_action = show
resource_instance = ArticleRepository ???

Try to change on ability.rb not on controller find method.

@steveklabnik
Copy link
Author

the instance is an @Article, though. ArticleRepository.new returns an Article. I verified this with the Rails.logger.info.
Thanks for pointing out that code, though, I'll have to dig into it.

@p8
Copy link

p8 commented Dec 12, 2011

Does the free column on articles default to false or nil ?

@steveklabnik
Copy link
Author

@p8 defaults to false.

$ cat db/migrate/20111212125155_add_free_to_articles.rb
class AddFreeToArticles < ActiveRecord::Migration
  def change
    add_column :articles, :free, :boolean, :default => false
  end
end

@p8
Copy link

p8 commented Dec 12, 2011

Maybe try free? instead
cannot :show, Article, :free? => false

@ryanb
Copy link

ryanb commented Dec 12, 2011

I don't think the issue is with the Ability class. You can test this in the console.

user = User.new # or fetch any user you want to test
article = Article.find(2) # fetch any article you want to test
ability = Ability.new(user)
ability.can? :show, article

What does that return for a free article?

@steveklabnik
Copy link
Author

The question mark makes no difference.

@steveklabnik
Copy link
Author

@ryanb, apparently, it's not there, yes:

ruby-1.9.3-p0 :001 > u = User.new
 => #<User id: nil, 
 ruby-1.9.3-p0 :002 > article = Article.find(2)
 => #<Article id: 2, body: "yo yo yo", created_at: "2011-12-12 12:25:12", updated_at: "2011-12-12 12:25:12", free: false> 
ruby-1.9.3-p0 :003 > article.free
 => false 
ruby-1.9.3-p0 :004 > ability = Ability.new(u)
 => #<Ability:0x0000010270d4d0 ...
ruby-1.9.3-p0 :005 > ability.can? :show, article
 => false 
ruby-1.9.3-p0 :006 > article = Article.find 1
 => #<Article id: 1, body: "Welcome! lolz", created_at: "2011-12-12 11:46:52", updated_at: "2011-12-12 12:52:49", free: true> 
ruby-1.9.3-p0 :007 > article.free
 => true 
ruby-1.9.3-p0 :008 > ability.can? :show, article 
 => true 

Sooooo okay. That helps. Now to figure out why the hell it's being odd in the controller.

@spoonito
Copy link

@steveklabnik for test purpose try load_and_authorize_resource on controller.

... or try to put put authorize! :show, @Article after fetch record on show.

@ryanb
Copy link

ryanb commented Dec 12, 2011

The issue is that authorize_resource call does not load the Ability record so there is no way for it to know what the free attribute is. When no ability is loaded it will check on the class like this (try in the console).

ability.can? :show, Article

That will return true because the user does have the ability to access an article, but this would need a second check on the article instance where the "free" attribute is available.

To fix this you can either switch the authorize_resource call to load_and_authorize_resource (which won't go through the ArticleRepository) or move the loading of the article into a before filter so it happens before the authorization like this.

class ArticlesController < ApplicationController
  before_filter :load_resource, :only => [:show]
  authorize_resource

private

  def load_resource
    @article = ArticleRepository.find(params[:id])
  end
end

I realize this is a bit confusing which is why CanCan 2.0 handles this much better (still in development in a separate branch).

@steveklabnik
Copy link
Author

@ryanb just confirmed while you were answering this, adding the authorize call after I load it from the repository makes it work. You're awesome, thanks so much. I was literally banging my head against this all morning.

@steveklabnik
Copy link
Author

And thanks to all the rest of you for helping me isolate bits and pieces all day. <3 <3 <3. You're awesome.

@ryanb
Copy link

ryanb commented Dec 12, 2011

BTW, the authorize_resource call is quite simple, it's primarily a before_filter which does this.

authorize!(params[:action], @article || Article)

So you may want to remove that call and before filter and just perform that authorization manually in the action after the resource is loaded.

class ArticlesController < ApplicationController
  # no authorize_resource
  def show
    @article = ArticleRepository.find(params[:id])
    authorize!(:show, @article)
  end
end

@steveklabnik
Copy link
Author

I'll have to balance which one is more clear. I'm just glad that I don't have to waste more time on this, but as you can tell, it's kinda important functionality ;)

@guilherme
Copy link

@steveklabnik, i've been thinking about why use the repository pattern instead of access directly into ActiveRecord? http://martinfowler.com/eaaCatalog/repository.html it says that repository is about to mediate the domain and data mapping layer(active record in this case). but in practice what are the benefits of using this? thank you.

@steveklabnik
Copy link
Author

@guilherme ... pay attention to my blog sometime next week.

@guilherme
Copy link

@steveklanik i'm curious about it .. i can't wait until next week. :( :~

@steveklabnik
Copy link
Author

Here's the one sentence answer: It decreases the coupling in your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment