in my rails app (I use devise and cancan), each (registered) user belongs to exactly one role ('Administrator' or 'Users') but to at least one group (something like 'Family', 'Friends', 'Co-workers'). At runtime, when a new folder (see below) is created, a habtm relation to one or many groups can be set, which defines who can access the folder. Selecting no group at all should result in a world-wide accessible folder (i.e. users do not have to be logged in to access these folders). But right now, I don't know yet, how to define such world-wide accessible folders in my ability.rb, because I do not know how to define "can read folders which have no groups associated to it".
The relevant snippet of my app/models/ability.rb looks like this:
user ||= User.new
if user.role? :Administrator
can :manage, :all
elsif user.role? :Users
# user should only be able to read folders, whose associated groups they are member of
can :read, Folder, :groups => { :id => user.group_ids }
else
# here goes the world-wide-accessible-folders part, I guess
# but I don't know how to define it:
## can :read, Folder, :groups => { 0 } ???
end
The relevant snippet of my app/controllers/folders_controller.rb looks like this:
class FoldersController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
Can someone give me a hint?
I had the same problem just the other day. I figured out the solution after reading the CanCan readme, which you should do if you haven't yet.
You can view my solution here: Context aware authorization using CanCan
To give you an answer more specific to your use case, do the follow:
In your application controller you need to define some logic which will pick your abilities.
class ApplicationController < ActionController::Base
check_authorization
def current_ability
if <no group selected logic> # Maybe: params[:controller] == 'groups'
#current_ability = NoGroupSelectedAbility.new(current_user)
else
#current_ability = GroupSelectedAbility.new(current_user)
end
end
# Application Controller Logic Below
end
You'll then need to create a new ability (or abilities) in your app/models/ folder. You can also do cool stuff like this:
if request.path_parameters[:controller] == groups
#current_ability = GroupsAbility.new(current_group_relation)
end
Where current_group_relation is defined in app/controllers/groups_controller.rb. This will give you specific abilities for specific controllers. Remember that a parent classes can call methods in child classes in Ruby. You can define a method in your controller, and call it from ApplicationController, as long as you are certain what controller is currently being used to handle the request.
Hope that helps.
EDIT: I wanted to show you what a custom ability looks like.
# File path: app/models/group_ability.rb
class GroupsAbility
include CanCan::Ability
# This can take in whatever you want, you decide what to argument to
# use in your Application Controller
def initialize(group_relation)
group_relation ||= GroupRelation.new
if group_relation.id.nil?
# User does not have a relation to a group
can :read, all
elsif group_relation.moderator?
# Allow a mod to manage all group relations associated with the mod's group.
can :manage, :all, :id => group_relation.group.id
end
end
end
Related
I want 3 user levels as Admin ,Manager,Customer in my rails application.
So i've created 3 devise models as admin,manager and customer.
And in my application there are models and controllers for product,delivery,services.
And I want to set access levels to each models.
So Admin have access to all models, controllers
Manager have access to Product, Delivery
Customer have access to Services
how can i write the ability model to match these requirements
I've written it as follows.Don't know whether it's correct.
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)
if user.admin?
can :manage, :all
elsif user.manager?
can :manage, product ,delivery
elsif user.customer?
can :manage, services
end
end
AND PLEASE HELP ME TO WRITE THE CODE FOR THE MODELS TO RESTRICT DIFFERENT USER ROLE ACCESS.
PLEASE BE KIND ENOUGH TO HELP ME!
I think the easiest way to tackle this would be in your controller. Let us say you had a book model with controller, and only wanted to allow admins to access this part. It's probably best to create a method in your bookings controller, and call it using the before action method:
class BookingsController < ApplicationController
before_action :check_admin
def check_admin
return unless admin_signed_in?
redirect_to root_path, error: 'You are not allowed to access this part of the site'
end
end
This will perform the check_admin method every time anything happens in the bookings controller, and will redirect to your root path unless an admin is signed in. Devise also comes with user_signed_in? and manager_signed_in? helpers if you've created models with those names.
You can tailor this more by deciding which controller actions will perform the action, for example
before_action :check_admin, only: [:edit, :create, :delete, :update, :new]
Would only perform the check before those controller actions, and allow anyone to access the index and show actions.
Having trouble figuring out how to set up my different roles with cancancan abilities. I have a model "Business" which has many users with a role of either :owner, :manager or :employee.
Im trying to make it first that if they don't belong_to that business they can't see anything for that business. And second I want to limit functionality based on which role they have.
I guess I could do this within the views by using if statements and only showing them the things they have access to, but wondering if there is a better way with cancan
inside your ability.rb
class Ability
include CanCan::Ability
def initialize(user)
alias_action :create, :read, :update, :destroy, :to => :crud
if user
if user.role == "manager"
can :crud, Business, :id => user.business_id
# this will cek whether user can access business instance (id)
elsif user.role == "owner"
can :manage, :all
end
end
end
end
inside your controller you can do checking with 2 ways
step 1: with load_and_authorize_resource, this will automatically check all 7 rails method
class BookingsController < ApplicationController
load_and_authorize_resource
# this before filter will automatically check between users and resource
# rails method here
def show
end
end
step 2: check manually with authorize inside each method
def show
#business = Business.find(params[:id])
authorize! :read, #business
end
Definitely read through cancan's wiki as I'm not 100% on this, but I think the solution will look something like this:
def initialize(user)
user ||= User.new
if user.has_role?(:owner)
can :read, Business, Business.all do |business|
business.id == user.business_id
end
elsif user.has_role?(:whatever)
# etc
end
end
Then just check authorize! in the controller in the normal cancan way. As for showing them appropriate functionality in views, you can either do a bunch of if statements in the view any maybe try to use partials to make it all look palatable, or check in the controller and render different views based on role, but yeah, there's gotta be if statements somewhere.
The best way is to always use "incremental" permissions. Consider that cancancan starts already with the assumption that your users have no right on Business, so you can give them "incremental" permissions based on their role.
An example would be:
# give read access if they have any role related to the business
can :read, Business, users: { id: user.id }
# give write access if they are manager
can [:edit, :update], Business, business_users: { role: 'manager', user: { id: user.id } }
# give destroy permissions to owners
can [:destroy], Business, business_users: { role: 'owner', user: { id: user.id } }
My rails app has a few cab operators and they have a few cabs associated with them, and they are related as follows:
class Operator < ActiveRecord::Base
has_many :cabs
end
I have used Devise as my authentication gem. It authenticates users, admins and super admins in my app. I have created separate models for users, admins and super admins (and have not assigned roles to users per se).
I now wish to add the authorization feature to the app, so that an admin (who essentially would be the cab operator in my case) can CRUD only its own cabs. For e.g., an admins belonging to operator# 2 can access only the link: http://localhost:3000/operators/2/cabs and not the link: http://localhost:3000/operators/3/cabs.
My admin model already has an operator_id that associates it to an operator when an admin signs_up. I tried to add the authorization feature through CanCan, but I am unable to configure CanCan to provide restriction such as the one exemplified above.
I also tried to extend my authentication feature in the cabs_controller, as follows:
class CabsController < ApplicationController
before_action :authenticate_admin!
def index
if current_admin.operator_id != params[:operator_id]
redirect_to new_admin_session_path, notice: "Unauthorized access!"
else
#operator = Operator.find(params[:operator_id])
#cabs = Operator.find(params[:operator_id]).cabs
end
end
But this redirects me to the root_path even if the operator_id of the current_admin is equal to the params[:operator_id]. How should I proceed?
EDIT:
Following is my routes.rb file:
Rails.application.routes.draw do
devise_for :super_admins
devise_for :users
resources :operators do
resources :cabs
end
scope "operators/:operator_id" do
devise_for :admins
end
end
I have three tables: users, admins and super_admins. I created these coz I wanted my admins to hold operator_ids so that the admins corresponding to an operator can be identified. Also, I wanted the admin sign_in paths to be of the type /operators/:operator_id/admins/sign_in, hence the tweak in the routes file.
Unfortunately, initially I didn't understand that you actually have 3 different tables for users and (super)admins... Not sure that Pundit can help you in this case, but I'll keep the old answer for future visitors.
Coming back to your problem, let's try to fix just the unexpected redirect.
Routes seems fine, so the problem can be one of this:
You're getting redirected because you're currently not logged in as an admin, so you don't pass the :authenticate_admin! before_action.
You say "even if the operator_id of the current_admin is equal to the params[:operator_id]", but this condition is probably not true. Can you debug or print somewhere the value of both current_admin.operator_id and params[:operator_id] to see if they're actually equals?
Another interesting thing, is that you have a redirect for new_admin_session_path in your code, but then you say "this redirects me to the root_path". Can you please double check this?
OLD ANSWER
If you want to setup a good authorization-logic layer, I advice you to use pundit.
You've probably heard about cancan, but it's not supported anymore...
Leave Devise managing only the authentication part and give it a try ;)
PUNDIT EXAMPLE
First of all, follow pundit installation steps to create the app/policies folder and the base ApplicationPolicy class.
Then, in your case, you'll need to create a CabPolicy class in that folder:
class CabPolicy < ApplicationPolicy
def update?
user.is_super_admin? or user.cabs.include?(record)
end
end
This is an example for the update action. The update? function have to return true if the user has the authorisation to update the cab (You'll see later WHICH cab), false otherwise. So, what I'm saying here is "if the user is a super_admin (is_super_admin? is a placeholder function, use your own) is enough to return true, otherwise check if the record (which is the cab your checking) is included in the cabs association of your user".
You could also use record.operator_id == record.id, but I'm not sure the association for cab is belongs_to :operator. Keep in mind that in CabPolicy, record is a Cab object, and user is the devise current_user, so implement the check that you prefer.
Next, in your controller, you just need to add a line in your update function:
def update
#cab = Cab.find(params[:id]) # this will change based on your implementation
authorize #cab # this will call CabPolicy#update? passing current_user and #cab as user and record
#cab.update(cab_params)
end
If you want to make things even better, I recommend you to use a before_action
class CabsController < ApplicationController
before_action :set_cab, only: [:show, :update, :delete]
def update
#cab.update(cab_params)
end
#def delete and show...
private
def set_cab
#cab = Cab.find(params[:id])
authorize #cab
end
And of course, remember to define also show? and delete? methods in your CabPolicy.
So after putting the following at the top of my Product Controller:
authorize_actions_for(Product)
...and this to the top of my Product.rb:
resourcify
include Authority::Abilities
...and this to the top of my User.rb
rolify
include Authority::UserAbilities
I was able to have this rule obeyed from application_authorizer.rb:
def self.default(adjective, user)
user.has_role? :admin
end
In other words only admin users could engage with Products in any way.
However, when I created a product_authorizer.rb like the following
class ProductAuthorizer < ApplicationAuthorizer
def self.creatable_by?(user)
user.has_role? :admin
end
def self.updatable_by?(user)
user.has_role? :admin
end
def self.deletable_by?(user)
user.has_role? :admin
end
end
I was not seeing the rules obeyed. I was seeing that my non-admin user still couldn't access the show action for a product. Shouldn't this be allowed given the above?
I tried adding this to Product.rb:
self.authorizer_name = 'StyleAuthorizer'
I also tried commenting out the main rule in app_app.rb
#def self.default(adjective, user)
# user.has_role? :admin
#end
...but still no glory
The very most basic default in Authority's source code always returns false. So unless you explicitly authorize non-admins in a self.readable method, they won't be able to access show pages.
Remove the other methods you have in your product_authorizer.rb file (they're not necessary)
and replace them with
def self.readable_by?(user)
true
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?