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
Related
I am using Rails 4, devise, Role Model and CanCanCan.
Is it possible to define an ability in ability.rb that is common to a number of roles?
For example, every logged in user can CRUD their own profile page? And then roles have specific abilities on top of that common ability?
How does that work? Do I need to create a role in Role Model for common abilities and then allow each user to have multiple roles, so that they get the common abilities as well as the role specific abilities?
For example, in my ability.rb, I have:
class Ability
include CanCan::Ability
def initialize(user)
alias_action :create, :read, :update, :destroy, :to => :crud
# Define abilities for the passed in user here. For example:
#
user ||= User.new # guest user (not logged in)
#users who are not signed in can create registration or login
# can read publicly available projects, programs and proposals
can :read, Project, {:active => true, :closed => false, :sweep => { :disclosure => { :allusers => true } } }
# {:active => true, :closed => false && :Project.sweep.disclosure.allusers => true}
# if user role is student
if user_signed_in?
can :crud, Profile, :user_id => user.id #[for themselves]
elsif user.try(:profile).present? && user.profile.has_role?(:student)
So, I want students to be able to read the same things that guests can read. Is there a way to say that students can do everything that new users and users who are signed in can do (as well as the student specific abilities)?
You can make a kind of composition in your roles through function calls like this
class Ability
include CanCan::Ability
def initialize(user)
# Define abilities for the passed in user here. For example:
#
user ||= User.new # guest user (not logged in)
#users who are not signed in can create registration or login
# can read publicly available projects, programs and proposals
# {:active => true, :closed => false && :Project.sweep.disclosure.allusers => true}
# if user role is student
if user_signed_in?
if user.try(:profile).present? && user.profile.has_role?(:student)
student
else
authenticated
end
else
anonymous
end
end
def anonymous
can :read, Project, {:active => true, :closed => false, :sweep => { :disclosure => { :allusers => true } } }
end
def authenticated
anonymous
can :crud, Profile, :user_id => user.id #[for themselves]
end
def student
authenticated
#other student abilities
end
#other roles follow the same principal
def teacher
authenticated
end
end
The authenticated function will contain the common abilities for any role and each role that needs it will just call (it's kind of inheritance where any student can do what the authenticated user can plus his abilities)
I have here add an example ability class for your understanding. You can understand easily the code and read comments. Your code seems not good, I can point one thing, you should not manage role through profile, you should use user for assign or manage roles.
If you want to give same ability to a group of user then you can use this type of || condition user.has_role?(:role_one) || user.has_role?(:role_two) and pass ability block as can :manage, [SomeClassName, SomeClassName].
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
#Only same user can mange his Profile
can :manage, [Profile], :user_id => user.id
#Give rule wise permission
if user.admin?
can :manage, :all
elsif user.has_role?(:some_role_name)
can :manage, [SomeClassName]
elsif user.has_role?(:role_one) || user.has_role?(:role_two)
can :manage, [SomeClassName, SomeClassName]
else
can :read, :all
end
end
end
Hope this will help you complete your task.
I am using this https://github.com/ryanb/cancan/wiki/Role-Based-Authorization#alternative-role-inheritance Works fine for me
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.
There is only admin interface in my app and I use AdminUser model. The admin users can have different roles.
I want to change the resource retrieval based on admin role. I added to my ActiveAdmin register block:
#app/admin/payments.rb
scope_to :current_admin_user
And I expect I could write something like:
#app/models/admin_user.rb
def payments
case self.role
when role == 'manager'
Payments.where('...')
when role == '...'
end
end
But this doesn't work and always shows all the resources.
Any idea how can I get this work?
Finally, I used the scoped_collection method
ActiveAdmin.register Payment do
...
controller do
def scoped_collection
#roles which need to be scoped
if current_admin_user.role == 'accountant' or current_admin_user.role == 'manager'
resource_class.send(current_admin_user.role)
end
end
end
And then just define the scopes in the model:
class Payment < ActiveRecord::Base
scope 'accountant', where('...')
scope 'manager', where('...')
...
end
I found the better solution, then.
Authorization adapter do just fine, and there is not need for scopes. For example, with CanCan:
class Ability
include CanCan::Ability
def initialize(user)
#read/manage actions
if user.role == 'manager'
can :manage, Payment, :accessible_by_manager => true
elsif user.role == 'accountant'
can :read, Payment, :accessible_by_accountant => true
else
#can read all
can :read, Payment
end
end
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 have a Post model with a :published attribute (boolean) and a User model with a role attribute (string). There are three roles: ROLES = %w[admin publisher author]
I don't want users whose role is author to be capable of setting, or editing, the :published field on the Post model.
I'm using CanCan (and RailsAdmin gem) and my simplified Ability.rb file looks like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role? :admin
can :manage, :all
elsif user.role? :publisher
can :manage, Post
elsif user.role? :author
# I want to prevent these guys from setting the :published attribute
end
end
end
Anyone got any tips for doing this sort of thing?
So far it is not possible. But according to this: https://github.com/ryanb/cancan/issues/326 this feature should be in cancan 2.0.
Update: you can see this on CanCan 2.0 branch here: https://github.com/ryanb/cancan/tree/2.0 in section "Resource Attributes"
Check out this post: How do I use CanCan with rails admin to check for ownership
It shows how to make a field not visible based off a users role.
UPDATE
I was able to set options in rails admin with this code:
config.model User do
edit do
configure :organization do
visible do
bindings[:view]._current_user.max_role_name != 'admin' ? false : true
end
end
configure :organization_id, :hidden do
visible do
true if bindings[:view]._current_user.max_role_name != 'admin'
end
default_value do
bindings[:view]._current_user.organization_id if bindings[:view]._current_user.max_role_name != 'admin'
end
end
include_all_fields
end
end
This configuration will hide the organization field if the logged in user is not an admin. It will then show an organization_id field ( set to type='hidden' ) and set the default value.
Hope this helps someone.
Until CanCan 2.0 comes out, I've solved this by creating a subclass of the model with restricted accessibility, something like:
class AuthorPost < Post
attr_protected :published
end
And then give authors access to AuthorPosts: can :manage => AuthorPost
Then in your controller, you can set the resource you want in a before_filter:
before_filter :set_resource
...
private
def set_resource
if current_user and current_user.author?
#resource = AuthorPost
else
#resource = Post
end
params[:post] ||= params[:author_post]
end
One last caveat: you won't be able to use load_and_authorize_resource in that controller. You'll have to do that manually, as detailed here: https://github.com/ryanb/cancan/wiki/Controller-Authorization-Example
You'll need to replace Project with #resource.
I'm on the fence as to whether this is more or less effective than the method described in the railscast. For my purposes, it left the original model totally intact, so my other code wasn't affected--and just allowed me to give some users fewer editable fields.
There is a way, I did something like this in my project. But CanCan is not entirely the answer. What you need to do is make attr_accessible in your model dynamic based on user role, so if you're an admin, then you're allowed to update the published field. If not, then giving the field a new value simply won't take when the model saves.
Railscasts comes to the rescue once again: http://railscasts.com/episodes/237-dynamic-attr-accessible
Following getting the backend part of that implemented, then you can do something about the frontend form by wrapping the publish field in the View with a roles check or something to show or hide the field based on the user. Rough example of my implementation...
<% if current_user.roles.where(:name => ['Administrator','Editor']).present? %>
<%= f.label :display_name %>
<%= f.text_field :display_name %>
<% end %>