Rails 3.2 CanCan - Restrict access to projects url not Owned - ruby-on-rails

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.

Related

Ruby on Rails CanCan Gem

I am a bit confused regarding CanCan Gem. I basically understand how to set up abillity.rb. For example lest say we have the following code:
// in abillity.rb
user ||= User.new
can [:update, :destroy, :edit, :read], Book do |book|
book.dashboard.user_id == user.id
end
And then lets say we have the following books controller:
// books_controller.rb
load_and_authorize_resource
def destroy
if can?(:destroy, #book)
#book.destroy!
redirect_to happy_world_path
else
redirect_to not_happy
end
end
My question is: Do we need to check 'can?(:destroy, #book)'?
From my understanding 'load_and_authorize_resource' will not even allow access to this method if we don't have abillity to destroy it.
Yo do not need to add if can?(:destroy, #book) in your action if you use load_and_authorize_resource
Like the README say
Setting this for every action can be tedious, therefore the load_and_authorize_resource method is provided to automatically authorize all actions in a RESTful style resource controller.
If an user without authorization try to destroy, he get a unauthorized response ( not remember if is a 401 code)
Maybe you can use if can?(:destroy, #book) in your views, to do no show thte destroy button. Like also in Check Abilities & Authorization section

How to add this specific authorization feature to my rails app?

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.

How to secure user show page alongside user admin functions when using devise

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.

Rails 3, Cancan: defining ability on custom controller action acting on collection, not member

I have a RESTful controller for the model UserResource. I added a custom action called remote_update and I want to limit that action only if the user's id matches:
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :regular
can [:remote_update], UserResource, :user_id => user.id
end
I am using load_and_authorize_resource in the controller.
The problem is that users are still able to use that action even if their user id does not match. (To test, I am using Firebug and changing the hidden value of the id).
My route is as follows:
resources :user_resources do
collection do
post 'remote_update'
end
end
According to https://github.com/ryanb/cancan/wiki/Authorizing-controller-actions, when we have custom actions, Cancan tries to load the resource using the id, from the link:
def discontinue
# Automatically does the following:
# #product = Product.find(params[:id])
# authorize! :discontinue, #product
end
I don't have an id defined because it is a POST, not a GET or PUT.
THoughts on how to construct the ability? Thank you.
It looks like you are trying to do an update ('remote_update') with a POST. A POST is supposed to create, and thus should not normally have a populated id. Thus I would not expect CanCan to do that lookup for you.
I suggest that you either:
Manually find the product and authorize it in your discontinue method,
or
Use a PUT
btw, The ability looks correct to me.

CanCan Nested Resource Controller Authorization

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.

Resources