Rails Cancan and State MAChine - Authorizing States

Rails cancan and State Machine - Authorizing states

This wasn't actually as terrible as I thought, and I've done it in a way that keeps the code rather short and current_user out of my models (so big plus).

The trick was to call authorize again in the controller.

basically

def update
authorize! params[:object][:state_event].to_sym, @object unless params[:object][:state_event].empty?
.... etc
end

This way I can just aliases in Ability.rb. So users that can do the action will be authorized, and users that cannot will get the exception. This is also awesome as it's the same ability I'll be using on the button based actions.

The only caveat, is that you can't use @object.state_transistions to get a list of the available states the user can transition to, but it should be possible to do this via some sort of helper method.

UPDATE: though getting those states in a view like layer is easy enough

i'm using simple form so I just a collection input such that

 ..... collection: @object.status_transistions.select{|t| can? t.event, @object}

Which leaves the select with all the transistions the object can go through, and that the user is also authorized to do :).

Transitions class (state machine) get a list of possible transitions

I can't see any obvious way to enumerate possible-next-states, but you can query the available events like this:

YourClass.state_machines[:default].events_for(:active)
=> [:disable]

(If you have more than one state machine there will be additional members in the YourClass.state_machines Hash)

Simple states in rails - state_machine

It'll cost you $9, but Ryan Bates did a screencast about this very topic not too long ago: http://railscasts.com/episodes/392-a-tour-of-state-machines.

Well worth watching.

Given a record retrieve users authorized to :read it via cancan

I am averagely familiar with Cancan. Having said that, this is what I could think of:

@myrecord = MyRecord.find(1)

authorized_user_ids = User.find_each do |user|
user.id if Ability.new(user).can? :read, @myrecord
end.compact

puts authorized_user_ids
# => [1, 2, 5, 10, 78]

Note however, this is extremely inefficient because it will loop through all users. You might want to perform caching this with a separate table/model that you would implement.

Design pattern: aggregate state for parent from child states in Rails

Probably the best solution would be to trigger a meeting status update when the Participant model is updated:

class Participant < ActiveRecord::Base
belongs_to :meeting
after_update :update_meeting_after_change

def update_meeting_after_change
self.meeting.update_status if self.status_changed?
end
end

class Meeting < ActiveRecord::Base
has_many :participants

def update_status
#logic here
end
end

self.status_changed? will return true if the Participant's status attribute changed during the update

Is it possible to use cancan in a model?

You can define abilities in cancan against any methods provided by the model. State machine transitions are themselves methods provided by the model, so just set up your abilities as you would for any other methods.

For example, given a simple model:

class Order < ActiveRecord::Base

state_machine :initial => :new do

event :start_processing do
transition :new => :processing
end

event :complete_order do
transition :processing => :complete
end

event :escalate_order do
transition :processing => :escalated
end

event :complete_escalated_order
transition :escalated => :complete
end

state :new
state :processing
state :escalated
state :complete
end

end

You might define abilities like this:

class Ability

if user.role? :orderer
can [:start_processing, :escalate_order, :complete_order], :orders
end
if user.role? :manager
can :complete_escalated_order, :orders
end

end

EDIT - I should have added, that you would then use these abilities in your controllers handling the user requests:

class OrdersController < ApplicationController

def complete
@order = Order.find_by_ref(params[:id])

if @order.can_complete_order?
authorize! :complete_order, @order
@order.complete_order
elsif @order.can_complete_escalated_order?
authorize! :complete_escalated_order, @order
@order.complete_escalated_order
else
redirect_to root_url, :notice => "Order cannot be completed"
end

redirect_to my_queue_path, :notice => "Order #{@order.ref} has been marked as complete."

end

State Machine, Model Validations and RSpec

It looks like you're running into a caveat noted in the documentation:

One important caveat here is that, due to a constraint in ActiveModel's validation
framework, custom validators will not work as expected when defined to run
in multiple states. For example:

 class Vehicle
include ActiveModel::Validations

state_machine do
...
state :first_gear, :second_gear do
validate :speed_is_legal
end
end
end

In this case, the :speed_is_legal validation will only get run
for the :second_gear state. To avoid this, you can define your
custom validation like so:

 class Vehicle
include ActiveModel::Validations

state_machine do
...
state :first_gear, :second_gear do
validate {|vehicle| vehicle.speed_is_legal}
end
end
end


Related Topics



Leave a reply



Submit