I've been using the two awesome gems, state_machine and cancan recently in my rails application but I'm curious as to the best way to integrate them cleanly.
Currently I've placed state transitions on buttons that go on actions authorized by the controller. This works perfectly, I can restrict who can perform that action.
I would like to give the user the ability to change the objects state in the edit form as well. I've noticed that state_machine will pick up on the state_event key in the hash, with the value of the action to perform (so it will go through all of state_machines callbacks).
This can be passed in with the params hash in update_attributes. Fantastic.
However only certain users should be able to change the object to certain states. How would I implement this?. The idea is that
params['state_event']=='move_to_x'
Should bail out for some users but be allowed for others. It also concerns me as until I implement this is the authorization part a clever user could post anything inside state event, even if they shouldn't be allowed too!.
You could do this in two ways. Either by putting a condition on the transition. Something like this:
transition :from => :parked, :to => :idling, :if => :valid_user
And create a valid_user method in your model.
def valid_user
if User.current_user.has_role?(xyz)
do baa
end
end
(User.current_user.has_role?(xyz)) is not a valid test - you'll need your own.
Or you can use a custom state machine validation:
state :first_gear, :second_gear do
validate :speed_is_legal
There's a caveat found about this in the docs:
http://rdoc.info/github/pluginaweek/state_machine/master/StateMachine/Integrations/ActiveModel
There's another interesting post here:
State Machine, Model Validations and RSpec
We use both methods successfully in our application.
-- EDIT FOR THE MASSES --
Thinking about the comment about using current_user in a model. We thought about it a re-read our code. In the one or two examples we were using this, we realised we could remove the current_user method completely, thus eliminating any security risks.
Instead of calling User.current_user, we swapped to:
self.users.first
This obviously assumes the model has_many users. You can then call this user's abilities
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 :).