i am working through with a light weight app that i want to implement a super simple role structure. currently i am working with enum to set the role, but i want to implement some sort of hierarchy.
enum role: [:registered_user, :active_registered_user, :admin, :account_admin, :vip]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :registered_user
end
This is how i define the roles on the users model, what i would like to do is have it so that registered_user < active_registered_user < admin < account_admin < vip.
So pretty much they have access to all the roles beneath them.
So if something said if current_user.admin? if i was an account_admin or vip it would return true.
I was thinking of doing some thing like this
def has_access?(user, access_role)
access_hash = {
"vip" => ['vip', 'account_admin', 'admin', 'active_registered_user', 'registered_user'],
"account_admin" => ['account_admin', 'admin', 'active_registered_user', 'registered_user'],
"admin" => ['admin', 'active_registered_user', 'registered_user'],
"active_registered_user" => ['active_registered_user', 'registered_user'],
"registered_user" => ['registered_user']
}
access_hash.[user.role].include?(access_role)
end
But then i have to run this method everywhere! is there a more ruby way of doing this?
Any help or design insights will be much appreciated.
Note:
enum allows for some cool active record calls:
user.admin! # sets the role to "admin"
user.admin? # => true
user.role # => "admin"
just if you wanted to see how i called user.role.
Enums map to a real number value. So perhaps something like:
def access_level
self.class.roles[role]
end
def do_a_thing_only_admins_can_do(user)
return unless user.access_level >= 2
do_thing
end
def do_a_thing_only_vips_can_do(user)
return unless user.access_level >= 4
end
Note: It's probably a better idea in the long run to use something like Cancan to manage authorization.
Related
I want to call user.skip_confirmation while his account is created by admin in admin panel. I want user to confirm his account in further steps of registration process, but not on create. The only idea I have is to override create in controller:
controller do
def create
user = User.new
user.skip_confirmation!
user.confirmed_at = nil
user.save!
end
end
The problem is, I have different attr_accessibles for standard user and admin, and it works, because ActiveAdmin uses InheritedResources:
attr_accessible :name, :surname
attr_accessible :name, :surname, invitation_token, :as => :admin
It doesn't work after I changed create (it worked before). How can I do what I want and still be able to use this :as => :admin feature?
I look at the answer and none is solving the issue at hand. I solve it the simplest way as shown below.
before_create do |user|
user.skip_confirmation!
end
controller do
def create
#user = User.new(params[:user].merge({:confirmed_at => nil}))
#user.skip_confirmation!
create! #or super
end
def role_given?
true
end
def as_role
# adapt this code if you need to
{ :as => current_user.role.to_sym }
end
end
something like that could work
EDIT: if you define role_given? to return true and as_role, InheritResources will use as_role to get the role information
also
controller do
with_role :admin
end
works, but this way you can't change the role given the user.
At your /app/models/user.rb
before_create :skip_confirmation
def skip_confirmation
self.skip_confirmation! if Rails.env.development?
end
I have looked at declarative_authorization, CanCan, and CanTango. They all are good in adding authorization to the application but I was wondering how does one add authorization to specific instance of a model i.e. a person can have a manage access in one project and only limited (read less than manage: limited update, etc) in another.
Could you please a better way? Apologies if my question sounds too trivial. It could be because I am new to RoR.
thanks,
John
As I know CanCan and declarative_authorization, and I implemented role-based authorizations with both, I recommend CanCan. Just my two cents.
Example (untested, unfortunately I cannot test here and I have no access to my code)
So let's say we have a structure like this:
class User < ActiveRecord::Base
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :users
# attributes: project_read, project_create, project_update
end
Then, CanCan could look like this:
class Ability
include CanCan::Ability
def initialize(user)
#user = user
#role = user.role
# user can see a project if he has project_read => true in his role
can :read, Project if role.project_read?
# same, but with create
can :create, Project if role.project_create?
# can do everything with projects if he is an admin
can :manage, Project if user.admin?
end
end
You can find all information you need in the CanCan wiki on github. Personal recommendation to read:
https://github.com/ryanb/cancan/wiki/Defining-Abilities
https://github.com/ryanb/cancan/wiki/Defining-Abilities-with-Blocks
https://github.com/ryanb/cancan/wiki/Authorizing-Controller-Actions
Basically you just need to extend the example above to include your roles through your relations. To keep it simple, you can also create additional helper methods in ability.rb.
The main mean caveat you may fall for (at least I do): Make sure your user can do something with a model before you define what the user can't. Otherwise you'll sit there frustrated and think "but why? I never wrote the user can't.". Yeah. But you also never explicitly wrote that he can...
class User < ActiveRecord::Base
belongs_to :role
delegate :permissions, :to => :role
def method_missing(method_id, *args)
if match = matches_dynamic_role_check?(method_id)
tokenize_roles(match.captures.first).each do |check|
return true if role.name.downcase == check
end
return false
elsif match = matches_dynamic_perm_check?(method_id)
return true if permissions.find_by_name(match.captures.first)
else
super
end
end
private
def matches_dynamic_perm_check?(method_id)
/^can_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
end
def matches_dynamic_role_check?(method_id)
/^is_an?_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
end
def tokenize_roles(string_to_split)
string_to_split.split(/_or_/)
end
end
Usage:
user.is_an? admin
user.can_delete?
I would like to delete a user with devise but be able to save its data just setting a flag like is_deleted to true and prevent login for those users.
What would be the best way to do this in devise ? I have seen some write-ups on this but they were for rails 2.x projects, Im on rails 3.1
If you want to prevent sign_in users whose deleted_at fields are not null, override active_for_authentication? on your devise resource model:
def active_for_authentication?
super && !deleted_at
end
You can set that deleted flag normally then override the find_for_authentication class level method in the user model.
The following should work
def self.find_for_authentication(conditions)
super(conditions.merge(:is_deleted => false))
end
Another approach is to use a default scope on your model.
Define a state on your User model, and add a default scope (Rails 3), this will scope all the queries on the User model with the condition from the scope:
app/models/user.rb
class User < ActiveRecord::Base
default_scope where("state != 'disabled'")
def disable!
self.update_attribute(:state, 'disabled')
end
end
Then, over-write the destroy method in your session controller, make sure you to grab the destroy code from the version of devise you're using:
*app/controllers/registrations_controller.rb*
class Users::RegistrationsController < Devise::RegistrationsController
# paranoid DELETE /resource
def destroy
resource.disable! # we don't remove the record with resource.destroy
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed if is_navigational_format?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
end
You can take it a step further by defining a state machine on your User model (be careful of how this will not cascade down the dependency tree, like a :dependent => :destroy would):
app/models/user.rb
class User < ActiveRecord::Base
include ActiveRecord::Transitions
state_machine do
state :passive
state :active
state :disabled, :enter => :bye_bye_user
event :activate do
transitions :from => :passive, :to => :active
end
event :disable do
transitions :from => [:passive,:active], :to => :disabled
end
end
default_scope where("state != 'disabled'")
end
#dgmdan: In regards to using :deleted_at => nil instead of false:
Devise's find_for_authentication method runs the conditions through a filter which stringifies values. What's happening is that the nil value being passed in for deleted_at is being converted to an empty string. This makes the query match no one, and thus to the end user it looks like the username and password were incorrect.
find_for_authentication calls find_first_by_auth_conditions like this:
def find_for_authentication(tainted_conditions)
find_first_by_auth_conditions(tainted_conditions)
end
Per the author, find_first_by_auth_conditions takes an optional second parameter, another conditions hash, but this one does not go through the filter. So what you can do is change the method like this:
def self.find_for_authentication(conditions)
find_first_by_auth_conditions(conditions, {:deleted_at => nil})
end
The second conditions hash with the :deleted_at => nil should be passed straight through to the ORM layer.
What's a cool way to protect attributes by role using declarative_authorization? For example, a user can edit his contact information but not his role.
My first inclination was to create multiple controller actions for different scenarios. I quickly realized how unwieldy this could become as the number of protected attributes grows. Doing this for user role is one thing, but I can imagine multiple protected attributes. Adding a lot controller actions and routes doesn't feel right.
My second inclination was to create permissions around specific sensitive attributes and then wrap the form elements with View hepers provided by declarative_authorizations. However, the model and controller aspect of this is a bit foggy in my mind. Suggestions would be awesome.
Please advise on the best way to protect attributes by role using declarative_authorizations.
EDIT 2011-05-22
Something similar is now in Rails as of 3.1RC https://github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb so I would suggest going that route now.
ORIGINAL ANSWER
I just had to port what I had been using previously to Rails 3. I've never used declarative authorization specifically, but this is pretty simple and straightforward enough that you should be able to adapt to it.
Rails 3 added mass_assignment_authorizer, which makes this all really simple. I used that linked tutorial as a basis and just made it fit my domain model better, with class inheritance and grouping the attributes into roles.
In model
acts_as_accessible :admin => :all, :moderator => [:is_spam, :is_featured]
attr_accessible :title, :body # :admin, :moderator, and anyone else can set these
In controller
post.accessed_by(current_user.roles.collect(&:code)) # or however yours works
post.attributes = params[:post]
lib/active_record/acts_as_accessible.rb
# A way to have different attr_accessible attributes based on a Role
# #see ActsAsAccessible::ActMethods#acts_as_accessible
module ActiveRecord
module ActsAsAccessible
module ActMethods
# In model
# acts_as_accessible :admin => :all, :moderator => [:is_spam]
# attr_accessible :title, :body
#
# In controller
# post.accessed_by(current_user.roles.collect(&:code))
# post.attributes = params[:post]
#
# Warning: This frequently wouldn't be the concern of the model where this is declared in,
# but it is so much more useful to have it in there with the attr_accessible declaration.
# OHWELL.
#
# #param [Hash] roles Hash of { :role => [:attr, :attr] }
# #see acts_as_accessible_attributes
def acts_as_accessible(*roles)
roles_attributes_hash = Hash.new {|h,k| h[k] ||= [] }
roles_attributes_hash = roles_attributes_hash.merge(roles.extract_options!).symbolize_keys
if !self.respond_to? :acts_as_accessible_attributes
attr_accessible
write_inheritable_attribute :acts_as_accessible_attributes, roles_attributes_hash.symbolize_keys
class_inheritable_reader :acts_as_accessible_attributes
# extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
include InstanceMethods unless included_modules.include?(InstanceMethods)
else # subclass
new_acts_as_accessible_attributes = self.acts_as_accessible_attributes.dup
roles_attributes_hash.each do |role,attrs|
new_acts_as_accessible_attributes[role] += attrs
end
write_inheritable_attribute :acts_as_accessible_attributes, new_acts_as_accessible_attributes.symbolize_keys
end
end
end
module InstanceMethods
# #param [Array, NilClass] roles Array of Roles or nil to reset
# #return [Array, NilClass]
def accessed_by(*roles)
if roles.any?
case roles.first
when NilClass
#accessed_by = nil
when Array
#accessed_by = roles.first.flatten.collect(&:to_sym)
else
#accessed_by = roles.flatten.flatten.collect(&:to_sym)
end
end
#accessed_by
end
private
# This is what really does the work in attr_accessible/attr_protected.
# This override adds the acts_as_accessible_attributes for the current accessed_by roles.
# #see http://asciicasts.com/episodes/237-dynamic-attr-accessible
def mass_assignment_authorizer
attrs = []
if self.accessed_by
self.accessed_by.each do |role|
if self.acts_as_accessible_attributes.include? role
if self.acts_as_accessible_attributes[role] == :all
return self.class.protected_attributes
else
attrs += self.acts_as_accessible_attributes[role]
end
end
end
end
super + attrs
end
end
end
end
ActiveRecord::Base.send(:extend, ActiveRecord::ActsAsAccessible::ActMethods)
spec/lib/active_record/acts_as_accessible.rb
require 'spec_helper'
class TestActsAsAccessible
include ActiveModel::MassAssignmentSecurity
extend ActiveRecord::ActsAsAccessible::ActMethods
attr_accessor :foo, :bar, :baz, :qux
acts_as_accessible :dude => [:bar], :bra => [:baz, :qux], :admin => :all
attr_accessible :foo
def attributes=(values)
sanitize_for_mass_assignment(values).each do |k, v|
send("#{k}=", v)
end
end
end
describe TestActsAsAccessible do
it "should still allow mass assignment to accessible attributes by default" do
subject.attributes = {:foo => 'fooo'}
subject.foo.should == 'fooo'
end
it "should not allow mass assignment to non-accessible attributes by default" do
subject.attributes = {:bar => 'baaar'}
subject.bar.should be_nil
end
it "should allow mass assignment to acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :dude
subject.attributes = {:bar => 'baaar'}
subject.bar.should == 'baaar'
end
it "should allow mass assignment to multiple acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :bra
subject.attributes = {:baz => 'baaaz', :qux => 'quuux'}
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow multiple accessed_by to be specified" do
subject.accessed_by :dude, :bra
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow :all access" do
subject.accessed_by :admin
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
end
To me this filtering problem is something that should be applied at the controller level.
You'll want to have something somewhere that defines how to decide which attributes are writeable for a given user.
# On the user model
class User < ActiveRecord::Base
# ...
# Return a list of symbols representing the accessible attributes
def self.allowed_params(user)
if user.admin?
[:name, :email, :role]
else
[:name, email]
end
end
end
Then, in the application controller you can define a method to filter parameters.
class ApplicationController < ActionController::Base
# ...
protected
def restrict_params(param, model, user)
params[param].reject! do |k,v|
!model.allowed_params(user).include?(k)
end
end
# ...
end
And finally in your controller action you can use this filter:
class UserController < ActionController::Base
# ...
def update
restrict_params(:user, User, #current_user)
# and continue as normal
end
# ...
end
The idea is that you could then define allowed_params on each of your models, and have the controllers for each of these use the same filter method. You could save some boilerplate by having a method in application controller that spits out a before filter, like this:
def self.param_restrictions(param, model)
before_filter do
restrict_params(param, model, #current_user) if params[param]
end
end
# in UserController
param_restrictions :user, User
These examples are intended to be illustrative rather than definitive, I hope they help with the implementation of this.
I'd use scoped_attr_accessible, which looks like just what you're looking for. Only you need to set the scope at the start of a request for all models.
To do that, use a before_filter in your application_controller.rb:
before_filter do |controller|
ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role
end
I would avoid every solution based on user access in model because it seems potentially dangerous. I would try this approach:
class User < ActiveRecord::Base
def update_attributes_as_user(values, user)
values.each do |attribute, value|
# Update the attribute if the user is allowed to
#user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute)
end
save
end
def modifiable_attributes
admin? ? [:name, :email, :role] : [:name, :email]
end
end
Then in your controller change your update action from:
#user.update_attributes(params[:user])
to
#user.update_attributes_as_user(params[:user], current_user)
Rails 3.1+ comes with a +assign_attributes+ method for this purpose - http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes.
I want to make a simple login, logout, also, different user have different user role. The Restful authentication seems works great, and the cancan is also very sweet for controlling user ability. But the question is how can I let these two works together. I watched the railcast, I was whether how to detect the user ability? Do I need to add a "ability" column in the user table?? Thank u.
http://railscasts.com/episodes/67-restful-authentication
http://railscasts.com/episodes/192-authorization-with-cancan
Look at the CanCan GitHub page: http://github.com/ryanb/cancan
Based on looking at both that and the RailsCast, I notice two things:
You define Ability as a separate model. There doesn't appear to be any necessary database columns.
There is no way you are forced to do roles, you are free to do this however you will.
With restful_authentication, just do the normal thing with your User model.
The most natural way to add CanCan would be to add an extra column to your User model called role or ability or something, then define methods as you see fit. Personally I'd probably do some kind of number system stored in the database, such as "0" for admin, "1" for high-level user, "2" for low-level user, etc.
Here's a few possibilities:
# Returns true if User is an admin
def admin?
self.role == 0
end
And:
# Returns true if User is admin and role?(:admin) is called, etc.
def role?(to_match)
{
0 => :admin,
1 => :super_user,
2 => :user,
3 => :commenter,
}[self.role] == to_match
end
Then in your Ability initialize method, you can use some kind of conditionals to set abilities, such as these snippets from the Railscast/readme:
if user.role? :admin
can :manage, :all
elsif user.role? :super_user
...
end
Or:
if user.admin?
can :manage, :all
else
...
end
I wrote a simple solution that works with CanCan too, just add a role_id:integer column to the User model:
# puts this in /lib/
module RolePlay
module PluginMethods
def has_roleplay(roles = {})
##roles = roles
##roles_ids = roles.invert
def roles
##roles
end
def find_by_role(role_name, *args)
find(:all, :conditions => { :role_id => ##roles[role_name]}, *args)
end
define_method 'role?' do |r|
r == ##roles_ids[role_id]
end
define_method :role do
##roles_ids[role_id]
end
end
end
end
then include this line in config/initializers/roleplay.rb
ActiveRecord::Base.extend RolePlay::PluginMethods
finally use it in your User model:
class User < ActiveRecord::Base
# ...
has_roleplay(:admin => 0, :teacher => 1, :student => 2)
# ...
end
now your model will have 2 new methods:
#user.role?(:admin) # true if user has admin role
#user.role # returns role name for the user