In my application, a user is allowed to assign another user as their "account manager" and the account manager would be allowed to modify all user info. I defined the following ability:
can :manage, User do |user|
user == current_user or user.account_manager == current_user
end
A user also has some nested resources (e.g: publications). I defined the following ability:
can :manage, Publication do |publication, user|
publication.user == current_user or user == current_user or user.account_manager == current_user
end
In the views I check using the following:
can? :update, #publication, #user_we_are_accessing
can? :create, Publication.new, #user_we_are_accessing.
Everything works just fine so far. My problem is with the controller. In my PublicationsController I added:
load_and_authorize_resource :user
load_and_authorize_resource :publication, :through => :user
However this always throws AccessDenied, because the check for publication is not passing the user object to the ability (trying to inspect the user object in the ability shows nil).
Any ideas how I can go about implementing this?
tl;dr: Using CanCan to authorize access to resources. User can assign another user as account manager. User has nested resources. Problem: nested resource is not accessible by account manager.
I solved this by doing the following:
1) replaced load_and_authorize_resource :publication with just loading the resource load_resource :publication
2) Added a before_filter :authorize after the load_resource call with the following implementaiton:
def authorize
raise CanCan::AccessDenied unless can? :manage, #publication, #user
end
This works, but I was hoping for a way that would solve this in the devise way, if there is such a thing. Thoughts and feedback are appreciated.
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 } }
I'm using devise and have let admins manage users with a Manage::UsersController.
This is locked down using cancan:
# models/ability.rb
def initialize(user)
if user admin?
can :manage, User
end
end
Normal users can have nothing to do with User other than through devise, so everything looks secure.
Now I want to give users a 'show' page for their own account information (rather than customising the devise 'edit' page). The advice (1,2,3) seems to be to add a users_controller with a show method.
I tried to give non-admins the ability to read only their own information with:
if user admin?
can :manage, User
else
can :read, User, :id => user.id # edited based on #Edwards's answer
end
However this doesn't seem to restrict access to Manage::UsersController#index, so it means that everybody can see a list of all users.
What's the simplest way to achieve this? I can see two options, (but I'm not sure either is right):
1) Prevent user access to Manage::UsersController#index
def index
#users = User.all
authorize! :manage, User # feels hackish because this is 'read' action
end
2) Over-write devise controller
Per this answer over-writing a devise controller could be a good approach, but the question is which controller (the registrations controller?) and how. One of my concerns with going this route is that the information I want to display relates to the User object but not devise specifically (i.e. user plan information etc.). I'm also worried about getting bogged down when trying to test this.
What do you recommend?
In your ability.rb you have
can :read, User, :user_id => user.id
The User model won't have a user_id - you want the logged in user to be able to see their own account - that is it has the same id as the current_user. Also, :read is an alias for [:index, :show], you only want :show. So,
can :show, User, :id => user.id
should do the job.
I would keep your registration and authentication as Devise controllers; then, create your own User controller that is not a devise controller.
In your own controller, let's call it a ProfilesController, you could only show the specific actions for the one profile (the current_user)
routes
resource :profile
profiles controller
class ProfilesController
respond_to :html
def show
#user = current_user
end
def edit
#user = current_user
end
def update
#user = current_user
#user.update_attributes(params[:user])
respond_with #user
end
end
Since it's always only editing YOU, it restricts the ability to edit or see others.
I'm trying to restrict access to Projects that a user did not create. This seems to be working fine with:
if user.has_role?(:Student)
can :create, Project
can :manage, Project, :user_id => user.id
end
(Side Question: Is there a better way to write that? ^^)
However, I can still access the URL: /users/5/projects
I just can't see the projects as expected. I'd rather it tell me that I cannot access the page, and redirect. I do have this in my application controller:
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
But I don't receive a redirection or error message. Do I need to add something else to the abilities to make that work?
I do have load_and_authorize_resource in both the ProjectsController and UsersController.
For the record, my routes look like this:
resources :users do
resources :projects
end
Try this one
if user.has_role?(:Student)
can :create, Project
can :manage, Project do |P|
user && P.user == user
end
It will check whether current user owns the project or not. If he doesn't own the project then he won't be able to modify it.
First condition is take just to check whether user object exist or not, you can also use exception handler there. Here's an example of that:
comment.try(:user) == user
If you want to enable the redirect behavior if the user cannot read any project in the current collection, override the index action and add extra enforcement:
def index
# #projects is loaded by the CanCan before filter load_and_authorize_resource
unless #projects.any?{|project| can?(:read, project)}
raise CanCanAccessDenied.new("no project readable there", :read, Project)
end
end
In index-like (collection) controller actions, CanCan enforces ACL via authorize :read, ModelClass.
https://github.com/ryanb/cancan/wiki/Checking-Abilities see "Checking with Class" section.
As you can read, if there is the possibility that the user to :read any of the ModelClass instances (even if these instances do not yet exist) the query authorize :action, ModelClass will authorize.
Given your URL /users/5/projects and routes resources :users do resources :projects end I belive this action is an index on projects for a specific user. So the CanCan index action will authorize, given can :manage, Project, :user_id => user.id, so there can exist projects the user can :read as such => authorize. Later on in the view I belive you authorize each specific project instance can? :read, project, and there is where they get filtered, as such the page remains empty.
I'm using Rails 3 with Devise for user auth. Let's say I have a User model, with Devise enabled, and a Product model, and that a User has_many Products.
In my Products controller I'd like my find method to be scoped by current_user, ie.
#product = current_user.products.find(params[:id])
unless the user is an admin user, i.e. current_user.admin?
Right now, I'm running that code in almost every method, which seems messy:
if current_user.admin?
#product = Product.find(params[:id])
else
#product = current_user.products.find(params[:id])
end
Is there a more elegant/standard way of doing this?
I like to do this as follows:
class Product
scope :by_user, lambda { |user|
where(:owner_id => user.id) unless user.admin?
}
end
this allows you to write the following in your controller:
Product.by_user(current_user).find(params[:id])
If you're running this code in a lot of your controllers, you should probably make it a before filter, and define a method to do that in your ApplicationController:
before_filter :set_product, :except => [:destroy, :index]
def set_product
#product = current_user.admin? ? Product.find(params[:id]) : current_user.products.find(params[:id])
end
I don't know what you use to determine if a user is an admin or not (roles), but if you look into CanCan, it has an accessible_by scope that accepts an ability (an object that controls what users can and can't do) and returns records that user has access to based on permissions you write yourself. That is probably really what you want, but ripping out your permissions system and replacing it may or may not be feasible for you.
You could add a class method on Product with the user sent as an argument.
class Product < ActiveRecord::Base
...
def self.for_user(user)
user.admin? ? where({}) : where(:owner_id => user.id)
end
Then you can call it like this:
Product.for_user(current_user).find(params[:id])
PS: There's probably a better way to do the where({}).