Let's say I have model User.
I want to #user to be 3 types : admin, regular and pro.
If I create column type:String, of course I can pass string, when creating new #user, 'admin'/'regular'/'pro'.
And then each time I operate with #user check something like
if #user.type == 'admin'
for my purposes.
But I feel that this is not how it is made by professional developers. (Am I right here?)
I want User model to have column which can only contain 3 specific values and not just any string.
Use an enum
The column should be in integer,with a default value (probably 0 in the following example). And in your model define it as:
enum type: { regular: 0, pro: 1, admin: 2 }
for example.
Now you automatically have these methods:
#user.regular?
#user.pro?
#user.admin?
Also, you can call #user.type and you will get a nice string representation:
> #user.type
=> "admin"
You can also query with a symbol like:
User.where(type: [:pro, :regular])
or
User.where.not(type: :admin)
etc.
This approach also works perfectly well with the CanCan gem.
If you want to assign roles in your application , you can simply use the Role-Based specific gems like Cancan and Rollify etc. The link to which is here. I will explain below how easy it would be to assign roles to users and yes how the professional developers do it.
In your User model , there will be a method as :
ROLES = %w[admin regular pro]
def role?(base_role)
ROLES.index(base_role.to_s) <= ROLES.index(role)
end
Now in your UsersController you can check whether :
if user.role? :admin
can :manage, Post
end
if user.role? :regular
can :manage, ForumThread
end
if user.role? :pro
can :manage, Forum
end
It's that easy ..!!!.
I hope this helped ..!!! :)
Related
I have three types of users "Admin," "Manager," and "Employee." Also, I have an accounts model. All of them can show these accounts. I want to limit employees' access to these accounts. I put the Ids of these accounts in an array and did a limit like the code below. My question is, I had 15 endpoints related to the accounts, Is the best way to do it like this, or may there be a solution that does it without editing all these endpoints?
def index
if #current_user.user_type == 'Admin'
#accounts = Accounts.all
#accounts = optional_paginate(#accounts)
elsif #current_user.user_type == 'Employee'
#accounts = Account.where(id: ACCOUNTS_IDS)
#accounts = optional_paginate(#business_accounts)
else
#business_accounts = optional_paginate(Account.all.includes(:account_managers).exclude_pending)
end
end
The ACCOUNTS_IDS is an array of accounts ids that the employees can access to them only.
Your strategy works, but I think you're sensing that it's not ideal.
You had to hard-code an array of Account IDs from your database, which is never a good idea. If you ever needed to migrate or seed or recreate the database, those IDs could change.
You have this code repeated 15 times(so it's not DRY)
If an account with an id in ACCOUNTS_IDS is ever deleted, imagine what will happen to each of these controller actions.
Adding to or removing from the accounts (ACCOUNTS_IDS) that Employees can see requires you to change your codebase and push those changes.
I'd suggest doing a few things differently:
1. Use an authorization gem
I personally love pundit, but there are other popular options
2. Don't hard-code database information
As mentioned before, the IDs can change and code that relies on a specific record having a specific ID (or relying on that specific record to exist) is brittle.
It's much better to add a column to your table to control this.
Here's an example migration:
add_column :accounts, :restricted, :boolean, default: true, null: false
ACCOUNTS_IDS = [1, 2, 3, 4] #just an e.g.
Account.where(id: ACCOUNTS_IDS).update_column(restricted: false)
:restricted could also be something easier to understand, e.g. :visible_to_employees
Now you can make scopes in your Account model:
# /models/account.rb
class Account < ApplicationRecord
scope :restricted, -> { where(restricted: true) }
scope :unrestricted, -> { where(restricted: false) }
...
end
This allows you to do Account.all to get everything, Account.restricted to get some, and Account.unrestricted to get the others.
This also allows you to add a view form where accounts could be made visible to Employees without having to change the code of your application.
3. Do this in the model, not in the controller
Lean towards having more code in your models and less code in your controllers.
(this will also help you when you implement a Pundit scope)
# models/user.rb
class User < ApplicationRecord
...
def authorized_accounts
case user_type
when 'Admin'
Account.all
when 'Employee'
Account.unrestricted
else
Account.all.includes(:account_managers).exclude_pending
end
end
Now your controller can be greatly simplified:
def index
#accounts = optional_paginate(#current_user.authorized_accounts)
end
I want create roles in my project. Each user can be: admin, registered or demo. Each role see different things.
How can I do that? What is the best gem to do roles?
This is a example in 'bad programming" of what I want:
def index
if current_user.role[:name] == 'admin'
#installations = Installation.all
elsif current_user.role[:name] == 'registered'
#installations = current_user.installations
elsif current_user.role[:name] == 'demo'
#installations = current_user.installations.first
else
end
end
Some gems that might be interesting for you :
rolify
role_model
If you decide to implement it yourself, then within some page you might want to change the content, for that you might want to do something like this :
Add a role to the user model using a migration :
class AddRoleToUsers < ActiveRecord::Migration
def change
add_column :users, :role, :string, default: :demo
end
end
Then in your app you can use it as follows:
def index
case current_user.role
when :admin
#installations = Installation.all
when :registered
#installations = current_user.installations
else
#installations = current_user.installations.first
end
end
You can also simply create a boolean admin for instance.
What you might want to do also is create some methods in your model so that you can call current_user.admin? or current_user.registered? . You can do that by doing (if you chose to use a string to store the role):
class User < ActiveRecord::Base
def admin?
self.role == "admin"
end
def registered?
self.role == "registered"
end
end
One advantage I see of having a role stored in a string is that if you have 5 roles for instance then you do not have 4 booleans (as when you store admin in a boolean) but only one string. On the long run you might want to store actually a role_id instead of a string and have a separate role model.
An excellent alternative pointed out by Jorge de Los Santos (another answer) is to use enum :
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
It is an excellent alternative because it will automagically add the methods described above such as current_user.admin? without hard coding them.
With your roles, you might want to do some authorization (admins can have access to specific pages, demo users are restricted to only a subset of pages, etc.). For this, you can use the gem called cancancan. You can look at this railscast to learn more about it. Also, you can have some infos here : How to use cancancan? .
There are plenty of solutions available to you.
Starting by gems:
https://github.com/RolifyCommunity/rolify
https://github.com/martinrehfeld/role_model
By using Devise architecture (in case you use it):
https://github.com/plataformatec/devise/wiki/How-To:-Add-a-default-role-to-a-User
By using enums in rails 4:
class AddRolesToUser < ActiveRecord::Migration
#add_column 'role', :integer, default: 0 to the users table
end
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
That will enable role methods.
user = User.find(1)
user.role #:demo
user.admin? #false
user.registered? #false
And consequently:
if user.admin?
#somethig
elsif user.registered?
#another something
else
#another another something.
And last but not least, what you are searching is not the manage roles solution, is the manage permissions solutions:
https://github.com/ryanb/cancan
Add a boolean, :admin to your User model.
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, deafult: false
end
end
Create a method for a registered user to separate them from demo users, such as verifying their email, providing a home address and phone number, filling out a profile, etc. This is up to you though, first you need to decide how a registered and demo user should be different.
The CanCan gem adds authorization to your project, and is especially useful if you want to implement multiple roles with differing abilities. When used with an authentication system like devise, you get a full suite of capability for your site.
You're in full control of what roles you want to define and what abilities they have. CanCan manages tracking, assignment, and querying of roles, and then gets out of your way to let you build what you need.
You can find the CanCan gem in Github: https://github.com/ryanb/cancan
It's simple to use, and the documentation is straightforward and easy to follow.
Using a combination of a few tutorials and screencasts to set up Rails and CanCan to work with Devise etc. I've established User, Role, and the UserRole joining model for the has_many :through connection. And I only want some users to be able to assign certain roles, or create new accounts with those roles, etc.
Based on the documentation here, I feel like this should be pretty straightforward. What I have is essentially this:
if user
if user.role? 'admin'
can :manage, User
elsif user.role? 'submanager'
can :manage, User, roles: { name: ['basic', 'premium'] }
end
else
can :create, User, roles: { name: ['basic'] }
end
And then I'd simply test it in the form with:
- if can? :create, User, roles: { name: 'admin' }
= check_box 'user[role_ids]', 'admin'
Or, at least, that's the gist. And trust me I've tried every combination of singulars, plurals, role_ids, role, role: [ ]. So I don't think it's something as simple as that. No matter what I do, the can? check passes. Every time. Without fail.
I've found a lot of stuff out there on CanCan but I can't seem to find this specific thing. Is it because I'm trying to misuse CanCan, and I should be doing this some other way? Or … how do I do this?
I don't think this should be done in CanCan, as the control granularity of CanCan is on action. So if you really wanna to use CanCan to fulfill your intention, I think you should make the ”role change“ as a method call instead of a form submission.
the controller file
class UserController
def basic
user.change_role_to_basic
end
def premium
user.change_role_to_premium
end
end
the ability file
can :basic, User do
end
can :premium, User do
end
I've modified my devise table, User, to have a clearance column. This column is a number (1-3) that represents a user's permissions (read, read/write, full control). Unlike all the examples I'm reading, my Clearance (Role in the examples) is not a separate table with a relationship but is its own column in the Devise table (User). Its default value is 1.
My ability.rb looks like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.clearance.equal?("2")
can :create, Post
can :manage, Post, :user_id => user.id
else
can :read, :all
end
end
end
I've written this off of other examples I've seen (I'm really new to Rails but trying to not ask for help unless absolutely necessary) with the intent that users with a clearance value of 2 can create posts and manage only their posts. I've also included that, because I haven't written the code for 1 and 3 yet, that all other clearance numbers can read everything.
Apperantly, however, Cancan thinks that my user (confirmed clearance level of 2) falls under the "else" provision and therefore I can only read posts. I get a You are not authorized to access this page message if I try to make a new one.
I'm lost. Help?
May this be that the clearance column is an integer? in such a case you should write user.clearance == 2 and not as you wrote.
I am trying to implement specific object (row) authorisation using cancan, I want it to work in a way that a user can only make a change(update/edit) to a Record if he/she has the role for that specific Record. after consulting the cancan docs I tried doing the following:
class Ability
include CanCan::Ability
def initialize(user)
can :manage, Record do |record|
user.can_edit(record)
end
end
end
class User
has_many :assignments
has_many :roles_as_editor, :through => :assignments, :class_name => "Role", :source => :role, :conditions => {:edit => true}
def rec_as_editor
self.roles_as_editor.collect{ |x| Record.where(:cp_secondary_id => x.record_id) }.flatten.uniq
end
def can_edit(rec)
rec_as_editor.include?(rec)
end
end
The can_edit method takes in a Record object and ensures that a User has the role necessary to make a change to it by returning true or false. this method is tested and works correctly so the problem seems to be with the CanCan code because when i try editing a Record that the user dosent hold the role for it still allows me to make changes to the Record, anyone know why this wont work?
If you require any further information please let me know through a comment.
Thank You
Are you authorizing the resource in the controller?
you should have load_and_authorize_resource in your controller
or
def edit
#critical_process = CriticalProcess.find(params[:id])
#this here is what you use
authorize! :edit, #critical_process
end
in your edit method inside the critical process controller.
I personally prefer to keep this logic completely separate from models so that I don't have to dig into model code to find authorization issues. In other words, user.can_edit checks for authorization which is what the ability file is supposed to be in charge of. Shouldn't matter though... in this case I think you might have a problem inside the can_edit method. I have used code that looks nearly identical to yours without problems many times like this:
can :manage, Record do |record|
user.has_role?(:record_manager)
end
I suggest including your code for can_edit or use the debugger to see what value gets returned from can_edit.
I think the problem comes from the way you query for the records that are supposed to have the user as an editor.
I copy/pasted your code and built the other associations from scratch.
And testing it in the console it works as expected when I use it:
>> u = User.last
>> a = Ability.new(u)
>> a.can :edit, Role.last
false
The only thing I changed is the query for the records: it seemed to look for a record that owns the role (your Role has a record_id) but then looks for the same key under cp_secondary_id.
I think something is wrong in your query, but what depends on your schema and associations:
roles_as_editor.collect{ |x| Record.where(:cp_secondary_id => x.record_id) }.flatten.uniq
as I understood your code we are traversing associations like this:
User=>Assignment<=Role(boolean edit flag)<=Record