Skip to content
This repository has been archived by the owner on Dec 12, 2021. It is now read-only.

How to use CanCan when the permission has multiple conditions? #453

Closed
bhellman1 opened this issue Aug 27, 2011 · 11 comments
Closed

How to use CanCan when the permission has multiple conditions? #453

bhellman1 opened this issue Aug 27, 2011 · 11 comments

Comments

@bhellman1
Copy link

Ryan, I have a model that's permission is based on a variety of conditions. I've been going in circles to define the CanCan condition correctly and it continues to get very messy. I was wondering if you had a suggestion that might be helpful here?

Models:
User
Room (id, password_required (boolean)
UserRoom (user_id, room_id, banned (boolean)

User Stories:

  1. Any user can join a Room when password_required == false. Unless they have a UserRoom.banned == true record
  2. Any user can join a Room when they have a UserRoom record with banned == false
  3. User can not join a Room that is password required == true and they do not have an existing UserRoom record

So far I have:

def initialize(user, room, room_password)
.....
can :show, Room if room && user.try(:is_a_room_member, room) == true && user.try(:is_a_banned_room_member, room) == false

As you can see the above doesn't solve all the stories and it's already getting ugly.

Any suggestions on how this can be simplified/cleaner and work?

Thank you

@nimf
Copy link

nimf commented Aug 27, 2011

I'm not Ryan, but I hope my idea would be useful.
If is_a_room_member and is_a_banned_room_member methods would accept nil as room
then I think it might be:
can :show, Room if user.try(:is_a_room_member, room) unless user.try(:is_a_banned_room_member, room)

@bhellman1
Copy link
Author

Thanks nimf using that and solving for the user stories you mentioned above I now have:

can :show, Room if user.try(:is_a_room_member, room) || room.password_required == false unless user.try(:is_a_banned_room_member, room)

That is cleaner. Problem here is that when this gets kicked to the applicationController's rescue_from CanCan::AccessDenied do |exception|

I have no way of knowing if it was because the user was banned or if the Room requires a password. As all CanCan::AccessDenied gets if the Model not the object being access:

Room(id: integer, user_id: integer, title: string, password_required: boolean, password: string) etc...

Any ideas where I'll know why the user wasn't given permission to view the Room? Thanks

@nimf
Copy link

nimf commented Aug 27, 2011

Oh, I see.
Well, the only thing which comes to my mind is a dirty hack:

At first, || room.password_required == false could give no method found for nil, if the room is nil.
may be you should move this check into is_a_room_member method, or create a new helper, like:

def room_eligible?(user, room)
  unless room
    session[:cannot_reason] = "no such room"
    return nil
  end

  if user.try(:is_a_banned_room_member, room)
    session[:cannot_reason] = "user banned in this room"
    return nil
  end

  if room.password_required
    unless user.try(:is_a_room_member, room)
      session[:cannot_reason] = "room password required"
      return nil
    end
  end

  return true
end

Then you can check session[:cannot_reason] in your rescue_from block. And your Ability code will be like:

can :show, Room if room_eligible?(user, room)

And I don't know if user is nil can the room be viewed if password is not required? With my example it is allowed.

You can also move room_eligible? into User model and check ability like user.try(:room_eligible?, room)

@bhellman1
Copy link
Author

Thanks nimf, that's interesting, I'd also need to make another session variable to store the room.id...

@ryan, does this problem go away with the new CanCan 2.0? Thanks

@bhellman1
Copy link
Author

@nimf one more thing. Where would the helper live? It can't be in the model as the model can't use session. Ability.rb won't work as it to is a model. Where did you have in mind? Thanks

@bhellman1
Copy link
Author

Another challenge here is that I'm not sure how to right controller specs that can see where CanCan Rescue redirects. Is that possible?

Right now if CanCan sends an Access Denied the test spec test ends there and doesn't continue with what is in the "rescue_from CanCan::AccessDenied do |exception|" block ... Thoughts?

@nimf
Copy link

nimf commented Aug 28, 2011

Shame on me! Things are much simpler:

#/app/models/ability.rb
  def initialize(user)
    user ||= User.new # guest user (not logged in)

    can :read, Room do |room| room_eligible?(user, room) end
  end

  def room_eligible?(user, room)
    unless room
      raise CanCan::AccessDenied.new("Not such room!", :read, Room)
    end

    if user.try(:is_a_banned_room_member?, room)
      raise CanCan::AccessDenied.new("User banned in this room!", :read, Room)
    end

    if room.password_required
      unless user.try(:is_a_room_member?, room)
        raise CanCan::AccessDenied.new("Password required!", :read, Room)
      end
    end

    return true
  end

Then just look for exception.message in rescue_from.

@bhellman1
Copy link
Author

Thanks, I like that but it doesn't let the CanCan Rescue know which Room.id we are talking about. I ended up using a Session variable in the controller to track that but it feels dirty, inelegant.

@ryan, any thoughts?

@nimf
Copy link

nimf commented Aug 29, 2011

nobody will restrict you from adding id into the exception message, i.e.:

def room_eligible?(user, room)
    unless room
      raise CanCan::AccessDenied.new("Not such room!", :read, Room)
    end

    if user.try(:is_a_banned_room_member?, room)
      raise CanCan::AccessDenied.new("User banned in the room #{room.id}!", :read, Room)
    end

    if room.password_required
      unless user.try(:is_a_room_member?, room)
        raise CanCan::AccessDenied.new("Password required for room #{room.id}!", :read, Room)
      end
    end

    return true
  end

Then just parse it in rescue_from.

@treydock
Copy link

treydock commented Apr 3, 2012

@nimf Using that method was extremely helpful, thanks.

I added a wiki page as I couldn't find any that had examples of using custom methods, https://github.com/ryanb/cancan/wiki/Custom-Ability-Methods

@derekprior
Copy link
Collaborator

Looks like @nimf handled this one pretty well and on perusal, @treydock's wiki entry looks helpful for folks with similar questions. Closing this out.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants