| # 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 |
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
Seems User.new.is? :user would always be true. Or am I missing something?
@therabidbanana that code also allows you to see all Articles, logged in and not.
ruby-1.9.3-p0 :001 > User.new.is? :user
=> false
I've added my User class to the gist.
You can show where you fetch articles? or where you use the permission?
@spoonito updated. It's pretty basic.
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)
Try on ability.rb put can :show, ArticleRepository, :free => true
can :read instead of :show?
@divout read means :show and :index, and I have no index.
@spoonito, I think you're on to something...
@mattdenner, didnt' help :/
@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.
@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.
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.
Does the free column on articles default to false or nil ?
@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
Maybe try free? instead
cannot :show, Article, :free? => false
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, articleWhat does that return for a free article?
The question mark makes no difference.
@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.
@steveklabnik for test purpose try load_and_authorize_resource on controller.
... or try to put put authorize! :show, @Article after fetch record on show.
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, ArticleThat 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).
@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.
And thanks to all the rest of you for helping me isolate bits and pieces all day. <3 <3 <3. You're awesome.
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
endI'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 ;)
@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.
@guilherme ... pay attention to my blog sometime next week.
@steveklanik i'm curious about it .. i can't wait until next week. :( :~
Here's the one sentence answer: It decreases the coupling in your code.
@spoonito the try method does not work. And Article.find(1).free #=> true and Article.find(2).free #=> false, so...