Custom Permissions Not Working As Expected #594

Closed
partydrone opened this Issue Mar 28, 2012 · 2 comments

Comments

Projects
None yet
2 participants

I am having trouble getting custom actions to work.

What I expect to happen:
A user should only be able to read, create, update, destroy, and submit their own claims. The "Approve" and "Deny" buttons should only show up if the user is an approver.

What's actually happening:
The "Approve" and "Deny" buttons do display for the user, but they do not display for the approver. This is the opposite of what I'm expecting.

I've read through the documentation and various existing issues. I've tried several things, including spelling out which permissions each user has, explicitly (i.e., [:read, :create, :update, :destroy, :submit] instead of :manage). It makes the buttons go away for the user, but they still don't show up for an approver. What am I missing?

models/ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    can :read, Category
    can :manage, Claim, user_id: user.id
    cannot [:approve, :deny], Claim

    if user.has_role? :approver
      can [:read, :approve, :deny], Claim
    end
  end
end

controllers/claims_controller.rb:

class ClaimsController < ApplicationController
  # Restrict access
  load_and_authorize_resource
  skip_authorize_resource only: :new

  def indexend

  def showend

  def newend

  def editend

  def createend

  def updateend

  def destroyend

  ## STATE MACHINE EVENTS

  def submitend

  def approveend

  def denyend
end

views/claims/show.html.erb

<% if can? [:approve, :deny], @claim %>
<%= link_to 'Approve this claim', approve_claim_path(@claim), class: "button gradient" if @claim.can_approve? %>
<%= link_to 'Deny this claim', deny_claim_path(@claim), class: "button red gradient" if @claim.can_deny? %>
<% end %>
Owner

ryanb commented Mar 29, 2012

Have you tried testing the Ability class in the console? This will tell you if the problem lies there or elsewhere. See this the Debugging Abilities page for details.

After testing the Ability class in the console, I have confirmed that the class itself is behaving as expected, and that permissions are set up the way they should be, but the view is still acting weird. In the console, I grabbed the first user, who is an approver:

Loading development environment (Rails 3.2.2)
irb(main):001:0> user = User.first
  User Load (0.1ms)  SELECT "users".* FROM "users" LIMIT 1
=> #<User id: 1, login: "aporter", group_strings: "Wireless, VPN Users, VPN, Wave-HC, Marketing Group,...", name: "Andrew Porter", email: "andrew.porter@wavetronix.com", ou_strings: nil, auth_token: "CxKa5eF1xyEfU8IxhVdsKw", account_balance: #<BigDecimal:7fae4b7c7e18,'0.25E3',9(36)>, vision_balance: nil, created_at: "2012-03-19 21:56:43", updated_at: "2012-03-26 22:37:21">
irb(main):002:0> user.roles.map { |r| r.name }
  Role Load (0.2ms)  SELECT "roles".* FROM "roles" INNER JOIN "roles_users" ON "roles"."id" = "roles_users"."role_id" WHERE "roles_users"."user_id" = 1
=> ["admin", "approver"]

I grab a claim owned by this user:

irb(main):003:0> claim = Claim.find(10)
  Claim Load (4.8ms)  SELECT "claims".* FROM "claims" WHERE "claims"."id" = ? LIMIT 1  [["id", 10]]
=> #<Claim id: 10, state: "pending", total: #<BigDecimal:7fae4b1cf7e0,'0.105035E3',18(45)>, user_id: 1, batch_id: nil, created_at: "2012-03-28 20:35:03", updated_at: "2012-03-28 21:18:29">

I create an Ability instance for the user:

irb(main):004:0> ability = Ability.new(user)
=> #<Ability:0x007fae4b1a6228 @rules=[#<CanCan::Rule:0x007fae4fd1f538 @match_all=false, @base_behavior=true, @actions=[:read], @subjects=[Category(id: integer, name: string, description: text, percentage: decimal, created_at: datetime, updated_at: datetime)], @conditions={}, @block=nil>, #<CanCan::Rule:0x007fae4fd1f2e0 @match_all=false, @base_behavior=true, @actions=[:manage], @subjects=[Claim(id: integer, state: string, total: decimal, user_id: integer, batch_id: integer, created_at: datetime, updated_at: datetime)], @conditions={:user_id=>1}, @block=nil>, #<CanCan::Rule:0x007fae4fd1efe8 @match_all=false, @base_behavior=false, @actions=[:approve, :deny], @subjects=[Claim(id: integer, state: string, total: decimal, user_id: integer, batch_id: integer, created_at: datetime, updated_at: datetime)], @conditions={:user_id=>1}, @block=nil>, #<CanCan::Rule:0x007fae4fd1e598 @match_all=false, @base_behavior=true, @actions=[:read, :approve, :deny], @subjects=[Claim(id: integer, state: string, total: decimal, user_id: integer, batch_id: integer, created_at: datetime, updated_at: datetime)], @conditions={}, @block=nil>]>

Given the following permissions:

class Ability
  include CanCan::Ability

  def initialize(user)
    can :read, Category
    can :manage, Claim, user_id: user.id
    cannot [:approve, :deny], Claim, user_id: user.id

    if user.role? :approver
      can [:read, :approve, :deny], Claim
    end

    if user.role? :processor
      can :manage, Batch
    end

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

Here is what this approver can do with his own claim:

irb(main):005:0> ability.can? :manage, claim
=> true
irb(main):006:0> ability.can? :approve, claim
=> true
irb(main):007:0> ability.can? :deny, claim
=> true

If I grab a claim that the approver doesn't own:

irb(main):008:0> claim = Claim.find(11)
  Claim Load (0.2ms)  SELECT "claims".* FROM "claims" WHERE "claims"."id" = ? LIMIT 1  [["id", 11]]
=> #<Claim id: 11, state: "pending", total: #<BigDecimal:7fae4fd435f0,'0.105E3',9(36)>, user_id: 3, batch_id: nil, created_at: "2012-03-28 22:17:39", updated_at: "2012-03-28 22:24:38">

The permissions still check out:

irb(main):009:0> ability.can? :manage, claim
=> false
irb(main):010:0> ability.can? :read, claim
=> true
irb(main):011:0> ability.can? :approve, claim
=> true
irb(main):012:0> ability.can? :deny, claim
=> true

Except when I combine actions like this:

irb(main):013:0> ability.can? [:approve, :deny], claim
=> false

So, I can put the individual checks on each button, and that gets me where I need to be:

<%= link_to 'Approve this claim', approve_claim_path(@claim), class: "button gradient" if @claim.can_approve? && can?(:approve, @claim) %>
<%= link_to 'Deny this claim', deny_claim_path(@claim), class: "button red gradient" if @claim.can_deny? && can?(:deny, @claim) %>

NOTE: I am coupling the display of the buttons with a state machine, so I'm checking to see if the claim is in a state from which it can be approved, and if the user has the ability to approve it.

partydrone closed this Mar 29, 2012

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