I’ve been playing about with Devise and CanCan for rails authentication and authorization recently - this great pair of posts from Tony Amoyal have helped a great deal.
I have a User model and a Role model, and wanted to automatically add the :user role to each user whenever roles are assigned. The roles were set in the controller as follows:
if @user.save
@user.role_ids = params[:user][:role_ids]
...
end
… so I wanted to automatically add the :user role when @user.role_ids was set. Initially I tried adding a ‘before_filter’ to the User model:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
before_filter :add_default_roles
...
def add_default_roles
self[:role_ids] << Role.by_name(:user).id
end
...
end
Obviously this didn’t work - setting the habtm field in this case happens after the model is saved (as you can see in the 2 lines from the controller above).
Next I tried to over-ride the roles setter in the model :
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
...
def role_ids=(_role_ids)
_role_ids << Role.by_name(:user).id
self.role_ids = _role_ids
end
...
end
The obvious (in hindsight) problem with this is that it is recursive - setting self.role_ids from inside the setter calls itself again, and results in a stack level too deep error. This recursive call can be fixed by changing the line
self.role_ids = _role_ids
with:
self[:role_ids] = _role_ids
or even
write_attribute(:role_ids, _role_ids)
This fixes the recursion problem, but still doesn’t add the default role properly, due to some complexities with the way habtm is implemented.
After a lot of digging around I came up with a solution that works using alias_method_chain:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
...
# Make sure all users have at least the :user role.
def role_ids_with_add_user_role=(_role_ids)
_role_ids << Role.by_name(:user).id
self[:role_ids] = _role_ids
self.role_ids_without_add_user_role = _role_ids
end
alias_method_chain :role_ids=, :add_user_role
...
end
After some testing this seems to do exactly what we need - when @user.role_ids is set, the role_ids_with_add_user_role function is called, and adds the :user role to the list of role ids before saving.