I am using CanCan and have been researching how to get started. However, it seems that most of the tutorials aren't very specific, and don't suit my own needs. I'm building a social network where users can create projects and add other users to their projects, allowing those users to moderate that project.
I currently have a Role model with a string attribute, and a User model from devise. Where do I go from here?
I have seen this post, but it doesn't fully explain how to set up the roles and the relationship between the Role model and the ability.rb file from CanCan.
If you need me to be more specific, please say so! I'm not the greatest Rails developer ;)
Edit
I have seen the railscast on this, and it doesn't have a separate Role model which I would like to have. I have tried using Rolify, but people have said it is too complicated and that it's possible to do it in a more simple way. I also ran into some complications so I'd like to just use my own Role model.
Edit
I'm currently using rolify and the roles are working. I found my solution at: https://github.com/EppO/rolify/wiki/Tutorial
If your User-Role stuff is looking similar to following:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
# user model has for example following attributes:
# username, email, password, ...
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, :through => :user_roles
# role model has for example following attributes:
# name (e.g. Role.first.name => "admin" or "editor" or "whatever"
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
you can do following:
First, extend your User model with a few helper methods or something similar:
class User < ActiveRecord::Base
def is_admin?
is_type?("admin")
end
def is_editor?
is_type?("editor")
end
def is_whatever?
is_type?("whatever")
end
private
def is_type? type
self.roles.map(&:name).include?(type) ? true : false # will return true if the param type is included in the user´s role´s names.
end
end
Second, extend your ability class:
class Ability
include CanCan::Ability
def initialize(user)
if user
can :manage, :all if user.is_admin?
can :create, Project if user.is_editor?
can :read, Project if user.is_whatever?
# .. and so on..
# you can work with your different roles on base of the given user instance.
end
end
end
Alternatively you could remove your User-Roles has-many-through associations and replace it with the easy-roles gem - very useful. It is available on github: https://github.com/platform45/easy_roles
Now you should have an idea how you could work with cancan, roles and all the stuff together :-).
Related
I am trying to achieve role-based authorization in Rails.
What we require:
Roles should be dynamic, we should able to create, edit, or delete roles.
Permissions also should be dynamic.
Findings:
We can't use the pundit gem because its policies are static and we can't make it dynamic.
We can use the cancan gem and we can use it dynamically but I didn't get how it can be done? And how it works with `database?
It's my first project on the authorization part. We have rails as the back-end and vue.js as the front end. Whatever roles are there, on the database all data should be empty at first. We'll use the seed to create a super-admin role and give all permissions. Super-admin will create roles, edit roles, destroy roles, and also add permissions, edit and destroy permissions eventually.
If there is any other helpful method then please let me know.
Thanks.
Pundit vs CanCanCan
Your conclusions about CanCanCan and Pundit are just nonsense. Neither of them are "static" or "dynamic" and they have pretty much the same features. The architecture and design philosophy are radically different though.
CanCanCan (originally CanCan) is written as a DSL which was the hottest thing since pre-sliced bread back when Ryan Bates created CanCan 10 years ago. It scales down really well and is easy to learn but gets really ugly as soon as you reach any level of complexity. If anything doing "dynamic authorization" in CanCanCan is going to be a nightmare due its architecture. The ability class in CanCanCan is the god of all god objects.
Pundit is just Object Oriented Programming. In pundit your policies are just classes that take a user and resource as initializer arguments and respond to methods like show?, create? etc. Pundit is harder to understand initially but since its just OOP you can tailor it however you want. And since your authentication logic is stored in separate objects it scales up to complexity far better and adheres to the SOLID principles.
How do I setup a dynamic roles system?
This is you standard role system ala Rolify:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(role, resource = nil)
roles.where({ name: role, resource: resource }.compact).exists?
end
def add_role(role, resource = nil)
role = Role.find_or_create_by!({ name: role, resource: resource }.compact)
roles << role
end
end
# rails g model user_roles user:belongs_to role:belongs_to
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
# rails g model role name:string resource:belongs_to:polymorphic
class Role < ApplicationRecord
belongs_to :resource, polymorphic: true, optional: true
has_many :user_roles
has_many :users, through: :user_roles
end
You can then scope roles to resources:
class Forum < ApplicationRecord
has_many :roles, as: :resource
end
Rolify lets you go a step further and just defines roles with a class as the resource. Like for example user.add_role(:admin, Forum) which makes the user an admin on all forums.
How do I create a permissions system?
A simple RBAC system could be built as:
class Role < ApplicationRecord
has_many :role_permissions
has_many :permissions, through: :role_permissions
def has_permission?(permission)
permissions.where(name: permission).exists?
end
end
# rails g model permission name:string
class Permission < ApplicationRecord
end
# rails g model role_permission role:belongs_to permission:belongs_to
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end
So for example you could grant "destroy" to "moderators" on Forum.find(1) by:
role = Role.find_by!(name: 'moderator', resource: Forum.find(1))
role.permissions.create!(name: 'destroy')
role.has_permission?('destroy') # true
Although I doubt its really going to be this simple in reality.
If I understand your requirements correctly, you should be able to use Pundit to achieve this.
From what I understand,
Users have roles
Users can be assigned and unassigned roles at runtime
Permissions are given to either roles or users directly.
Permissions can be updated at runtime
So you can have something like,
class User
has_many :user_role_mappings
has_many :roles, through: :user_role_mappings
has_many :permissions, through: :roles
...
end
class UserRoleMapping
belongs_to :user
belongs_to :role
end
class Role
has_many :role_permission_mappings
has_many :permissions, through :role_permission_mappings
...
def has_permission?(permission)
permissions.where(name: permission).exists?
end
end
class RolePermissionMapping
belongs_to :role
belongs_to :permission
end
class Permission
...
end
And in your policy, you can check if any of the user's roles has the required permission.
class PostPolicy < ApplicationPolicy
def update?
user.roles.any? { |role| role.has_permission?('update_post') }
end
end
Edit:
Using the mapping tables, you can update the permissions for a role, and the roles for a user from an admin dashboard.
You can achieve dynamic roles with pundit. pundit let's you define plain Ruby objects with methods that are called to determine if a user has permission to perform an action. For example:
class PostPolicy < ApplicationPolicy
def update?
user.has_role('admin')
end
end
If you want a user to have multiple roles you could set up a has_and_belongs_to_many: :roles association on your User model.
See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
I know how to create an admin role/user : https://github.com/plataformatec/devise/wiki/How-To:-Add-an-Admin-role
What I am wondering though is if there are any advantages or disadvantages to the two options to consider when deciding between them. Can anyone supply any insight on this please?
Let me muddle the water a bit. I prefer to this via a Role table and a join table UserRole. This way I can define more than one role without adding another column/table to db.
class User
has_many :user_roles
has_many :roles, :through => :user_roles
def role?(role)
role_names.include? role
end
def role_names
#role_names ||= self.roles.map(&:name)
end
def role=(role)
self.roles << Role.find_or_create_by_name(role)
end
end
class UserRole
# integer: user_id, integer: role_id
belongs_to :user
belongs_to :role
end
class Role
# string: name
has_many :user_roles
has_many :users, :through => :user_roles
end
It really depends on what you wish to do with your admin role. The first option, I would say is a bit secure as the admin role is a unique model in itself.
The second option is straightforward and would help you get going with the least effort. However, if your users figure out the boolean variable and a way to set it, any user can become an admin and access areas you don't want them to.
I'm working on a project using RoR, a social message board (internet forum), in which every user can create multiple Boards and join multiple Boards from other users.
I didn't want to reinvent the wheel so I'm using Devise for Authentication and CanCan for Authorization. However I'm having some issues implementing CanCan because of the following:
class Board < ActiveRecord::Base
has_many :memberships
has_many :users , :through => :memberships
end
class User < ActiveRecord::Base
has_many :memberships
has_many :boards, :through => :memberships
end
class Membership < ActiveRecord::Base
ROLE = ['Administrator', 'Moderator','Member', 'Banned']
belongs_to :user
belongs_to :board
end
The role doesn't belong to the user himself, it belongs to the relationship between the user and the board, that is the Membership. So it's not enough knowing who is the current_user I also need to know which board is being displayed, so I think I would have to send the Membership instead of the user to the Ability class initializer? Any guidance would be greatly appreciated.
You're on the right path.
If you haven't already, create this as an entirely new Ability. e.g. BoardAbility. I've found it useful to not be shy about passing-in additional dependencies, and to have CanCan do as much of the evaluation that's reasonable.
class BoardAbility
include CanCan::Ability
attr_reader :requested_by, :requested_resource
def initialize requested_by, requested_resource
return nil unless (requested_by.is_a?(User) && requested_resource.is_a?(Board))
#requested_by = requested_by
#requested_resource = requested_resource
default_rules
end
private
def default_rules
# common abilities to all users
can :flag_offensive, :all
can :view_thread_count, :all
# find this user's role to this board to define more abilities
role = Membership.where(user_id: requested_by.id, board_id: requested_resource.id).pluck(:role).first
if ['Administrator', 'Moderator'].include? role
can :ban_users, Board, {id: requested_resource.id}
end
end
end
Then in your BoardController define a private method to signify that we aren't using the default CanCan Ability class.
def current_ability
#current_ability ||= BoardAbility.new(current_user, #board)
end
Then when you're in your BoardController, use the usual CanCan DSL.
authorize! :ban_user, #board
I'm new to rails, and I'm working on my second rails app.
The app will have different roles for users, but some users will have multiple roles.
Every user of the site will be an Artist. Some users will have the role of a moderator.
How would I structure this? In some PHP apps I've used, there is only one user, and then a database column for is_admin, etc. But I've looked at the source for rails apps and have seen separate models for User and Admin, etc. although I'm not sure why.
So, should I have a single User model with a role attribute, which could be Moderator, and then just call Users "Artists" in my views, routes, etc.?
Or should I have a User model, a Moderator model which inherits from it, and an Artist model which belongs_to User?
I'm really confused.
You can look for gems Devise and CanCan. This pair is really powerful combination. This makes two models User and Role. In Role you can create new roles, without creating new models for them. Although it creates model Ability, here you can define access rules for roles.
Manual:
http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/
Here you can find Devise's and CanCan's sources and wikies:
https://github.com/plataformatec/devise
https://github.com/ryanb/cancan
My models looks like this:
Role.rb
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
User.rb
class User < ActiveRecord::Base
has_many :accounts
has_and_belongs_to_many :roles
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :username, :password, :password_confirmation, :remember_me, :role_ids
def role?(role)
return !!self.roles.find_by_name(role.to_s.camelize)
end
end
Ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user
if user.role? :administrator
can :manage, :all
elsif user.role? :operator
can :read, Account
can :read, Server
elsif user.role? :customer
can :manage, Account
can :read, Server
end
end
end
In the controller you must add only this two lines:
class YourController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
...
end
If you need to have code that's specific to and role or such as an admin or moderator one other solution would be to create a base User model that all the other classes inherit from. You can then create an Admin class and a Moderator class that inherit from the User model. This would mean you can avoid constantly checking the users role in your code e.g. current_user.do_some_admin_thing if current_user.is_admin?. Your classes would look something like this
class User < ActiveRecord::Base
# base user methods in here
end
class Moderator < User
def do_moderator_thing
# perform a moderator task
end
end
class Admin < Moderator
def do_admin_thing
# perform an admin task
end
end
In this instance the User class has the most basic privileges, moderators can do everything users can plus the moderator specific methods and admins can do everything users and moderators can plus the admin specific methods.
All the different user roles would use the same table in the database but your concerns are neatly separated into classes which avoids excessive conditionals through your code checking what role the user is all the time.
Creating new users would be straightforward also Admin.new :name => 'bob' the Admin class then takes care of how a user is defined as an admin which provides a nice interface where you don't need to know the inner workings of the role system to interact with users.
I think you don't have to create different models because you don't have specific fields for each one. So you just have to set the "role" of each User. Two options : create a role table or add a role field in the table User. Both solutions work, the second is more flexible but less optimized.
But, in your particular case, you don't have a complex role management so you can find a simpler solution. If all of your users are artists you don't have to specify this in your code, it's contained in the implicit description of what a user is. So you just have to save if a user is an admin or not and I think the best solution is to create a boolean field "is_admin".
After that you will have to create some before_filter in your protected controllers, like that :
before_filter => :authorize, :only => :new, :edit, :create, :update, :destroy
def authorize
redirect_to :root if not current_user.is_admin?
end
And you can have simple requests like that :
#artists = User.all
#moderators = User.where(:is_admin => true)
If you look for a more complete authorization system you can check this small gem : https://github.com/ryanb/cancan
But I think it's not the case for the moment. If you have a simple problem look for a simple solution !
Although I agree the combination of Devise and CanCan is powerful and works. Let us look at it with a different perspective keeping Association and Delegation in mind.
Association: In object-oriented programming, association defines a
relationship between classes of objects that allows one object
instance to cause another to perform an action on its behalf.
Delegation: Delegation allows the behaviour of an object to be
defined in terms of the behaviour of another object. The term
'delegation' refers to the delegation of responsibility. The primary
emphasis of delegation is on message passing where an object could
delegate responsibility of a message it couldn't handle to objects
that potentially could (its delegates).
With that, what if we design our User and Roles like this. There is no Role class and the User doesn't inherit or specialise a particular class ( Artist, Admin ) instead, all the (Role) class contain the User object and delegate. The way I am thinking and way to implement with Rails is something like this:
class User < AR::Base
def user_method
end
end
class Artist < AR::Base
has_one :user
def artist_method
# perform an admin task
end
end
class Admin < AR::Base
has_one :user
def admin_method
# perform an admin task
end
end
This role class model is described by Francis G. Mossé in his article on Modelling Roles.
This is the basic setup, for the declarative authorization gem, I use. But you could just use this as is without the gem, if your authorization requirements aren't more than asking the kind of Roles the User has.
It does require a roles table, and such, so that might not really be your fancy.
class Role < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :roles
def role_symbols
roles.map { |r| r.title.to_sym }
end
def admin?
has_role?(:admin)
end
# include more role booleans or write some ruby magic to be DRY
# ...
def has_role?(r)
role_symbols.include?(r.to_sym)
end
end
# give or take
user = User.new
user.roles << Role.new :title => "admin"
user.roles << Role.new :title => "artist"
user.role_symbols # => [:admin, :artist]
user.admin? # => true
user.has_role?(:artist) # => true
You can have two models User and Role. And Role belongs to User.
Specify role of users (like admin, moderator) in Role model.
I'm working on a roles/permissions setup but can't seem to get it working as intended.
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :join_table => "users_roles"
has_many :permissions, :through => :roles
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :permissions, :join_table => "roles_permissions"
class Permission < ActiveRecord::Base
has_and_belongs_to_many :roles
What i would like is to be able to access Roles and Permissions directly from my user object (user.roles, user.permissions).
user.roles is working, but i cant seem to get user.permissions to work. Is there anyone who can help me out?
Another approach would be use plugins like Devise + CanCan. I do however have some concerns about using plugins for things as essential as authentication and authorization - what happens if they get discontinued? Anyone who has a view on this?
Thanks!
CanCan is for authorization. Authlogic and Devise are for authentication. Authorization and authentication are two different but usually related facets of a web application.
I have a feeling that you cannot use has_many :through to reference a has_and_belongs_to_many association. I think a has_many :through must reference a has_many association. I couldn't find any definitive info though. Maybe someone else knows?
I never roll my own authentication because Devise and Authlogic both do the job very well and are easy to extend (Devise especially). Best practices for secure hashing is built-in. OpenID and Facebook authentication are simple add-ons. Why re-invent the wheel? Worst case they go unsupported sometime in the future. To me that's no big deal because I still have the source code so I had nothing to loose and everything to gain.
If you don't need your permissions do be dynamic I would hard code your permissions (aka the actions that users in certain roles can perform) in to the CanCan abilities file. You probably don't need a database table for roles either, unless there is additional metadata you want to store. I recommend avoiding has_and_belongs_to_many associations because most applications will eventually require additional data be associated with the joining table. This would be one solution to consider. There are simpler and more complex ways to accomplish to same thing.
class User < ActiveRecord::Base
has_many :roles
end
class RoleAssignment < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :role_assignements
has_many :users, :through => :role_assigments
end
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.roles.include? Role.find_by_name('admin')
can :manage, :all
else
can :read, :all
end
end
end
I think you'd be better off using CanCan to achieve authentications with roles etc.
But if you still want to access "permissions" from user model, I am guessing you can do like this in User model:
def permissions
Permission.find(:all, :joins => {:roles => :users}, :conditions => ["users.id = ?", self.id])
end
Haven't tested though.
You should check this out too.