13
votes

I'm relatively new to rails (3), and am building an application, using CanCan, where there are 3 tiers of users.

  • Guest - unregistered visitor User
  • registered and logged in visitor
  • Admin - registered and logged in visitor with admin flag

My ability is bog-stock right now, copied from cancan docs, basically defining the guest role and the admin role

class Ability

    include CanCan::Ability

    def initialize(user)
        user ||= User.new # Guest user

        if user.is_admin?
            can :manage, :all
        else
            can :read, [Asana,Image,User,Video,Sequence]
        end
    end

end

I'm looking to add in the user role. Since I'm creating that throwaway user model, I thought about using new_record? to determine if the user is logged in or not. Something like:

class Ability

    include CanCan::Ability

    def initialize(user)
        user ||= User.new # Guest user

        if !user.new_record? and user.is_admin?
            can :manage, :all
        elsif !user.new_record? and !user.is_admin?
            can {registered user-y permissions}
        else
            can :read, [Asana,Image,User,Video,Sequence]
        end
    end

end

But, it just doesn't feel right. Seems kind of disassociated from, like, actual logged-in-ed-ness, and have concerns about whether its actually secure.

Looking for advice on a more elegant way to doing this.

Thanks!

5
Have you added the role? method to the User model?Shreyas
I'm not using Roles (as per github.com/ryanb/cancan/wiki/Role-Based-Authorization) - just trying to segment into 3 tiers. Are you suggesting that I delegate off determining "logged-in-ed-ness" to the User model? Doesn't seem right that the User model should know anything about being logged in.Edward M Smith

5 Answers

22
votes

Good question, I use a lower to higher permissions approach:

class Ability  
  include CanCan::Ability  

  def initialize(user)
    # Guest User 
    unless user 
      can :read, [Asana,Image,User,Video,Sequence]
    else
      # All registered users
      can {registered user-y permissions}
      # Admins 
      if user.is_admin?
        can :manage, :all
      end
    end 
  end  
end

This way if tomorrow you have other roles to integrate you can do it adding a case statement like so:

class Ability  
  include CanCan::Ability  

  def initialize(user)
    # Guest User 
    unless user 
      can :read, [Asana,Image,User,Video,Sequence]
    else
      # All registered users
      can {registered user-y permissions}
      # Different roles
      case user.role
      when 'admin'
        can :manage, :all
      when 'manager'
        can :manage, [Video, Image, Sequence]
      end
    end 
  end  
end
2
votes

So what you basically want is abilities for none logged in users, abilities for a logged in user and then abilities for a logged in admin?

Because the current user model is passed into the initialize you're going to have to test based on a property of the user and it would make sense to use a basic role property stored on the user model, eg

def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role == 'admin'
  # Admin roles
  can :manage, :all
elsif user.role == 'user'
  # Signed in user permissions
else
  # Guest permissions
  can :read, :all
end

end

So when a user signs up/registers you can default the role value to 'user' and then allow for some method to update this to 'admin' in a management interface. You could use a single admin? check on a user since this would be false for guests as well as normal logged in users.

1
votes

If the user object isn't a new_record it means it's stored in the database. To me that's sufficient enough to accept that it's a logged in user.

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new  

    # Guest users
    can :create, User

    # Members  
    unless user.new_record?
      can [:edit, :update], User, :id => user.id
    end

    # Admins
    if user.admin?
      can :manage, :all
    end
  end
end

Furthermore, sometimes admin functionality doesn't have to be so fancy - perhaps this will be all you need?

class User < ActiveRecord::Base
  # Alternatively, you can add an admin attribute flag
  def admin?
    ["[email protected]", "hisspouse.yourservice.com"].include?(email)
  end
end

Finally, although you are new to rails, I'll suggest that you code your own authentication from scratch. Using gems like Devise and Authlogic almost always seems to have it's downsides. And it doesn't really have to be that complicated. Ryan (the author of cancan) did a great screencast on this topic: http://railscasts.com/episodes/250-authentication-from-scratch

1
votes

If you use role inheritance, this pattern works well:

Ability

user ||= User::GUEST # guest user (not logged in)

# anyone
can [:read], Post

# any registered user
if user.role? :user
  can [:comment], Post
end

# editor
if user.role? :editor
  can [:create], Post
end

# admin
if user.role? :admin
  can [:manage], Post
end

User

GUEST = User.new.tap {|u| u.role = 'guest'}

ROLES = %w[guest user editor admin]
def role?(base_role)
  begin
    ROLES.index(base_role.to_s) <= ROLES.index(role.to_s)
  rescue
    raise "invalid role query '#{base_role}' against user role '#{role}'"
  end
end
0
votes

If you actually have a persisted user model, then that is a logged in user, otherwise they are a guest. Maybe you are over-thinking this?

class Ability
  include CanCan::Ability

  def initialize(user=nil)
    if user && user.is_admin?
      can :manage, :all
    elsif user
      can {registered user-y permissions}
    else # guest
      can :read, [Asana,Image,User,Video,Sequence]
    end
  end
end