Authorize with multiple action in CanCan - ruby-on-rails

I am trying to understand a bit better the capabilities of CanCan when it comes to authorization. Imagine this controller action:
def update
if can? :action, Model or can? :resolve, Model or can? :authorize, AnotherModel
# My Code here
respond_with #model
else
raise CanCan::AccessDenied.new(nil, :update, Model)
end
end
I got to this point while trying to find a solution to the above using authorize!. As far as I can see (also looking at the signature) authorize! only accepts one permission (action) and one subject, with an optional message, like this:
def authorize!(action, subject, *args)
# code
end
Is there a way which I may be overlooking to instruct authorize to check for multiple actions? Putting two authorize one after the other will act as an AND condition between permissions, what I would like is it to work like an OR condition, basically similar to the custom code above (which has the problem of raising the AuthorizationNotPerformed in CanCan, avoidable with skip_authorize_resource which is not something I would really like to do).

You can create an custom action and create as many or-conditions as you like.
can :my_update_action, Project do |project|
can?(:read, ModelX) || can?(:read, ModelY) || ...
end

In the end I added this rather nice solution to the ability class:
def multi_authorize!(*actions, message_hash)
message = nil
if message_hash.kind_of?(Hash) && message_hash.has_key?(:message)
message = message_hash[:message]
end
auth = false
actions.each do |act|
auth = auth || can?(act[0], act[1])
end
if !auth
message ||= unauthorized_message(actions[0][0], actions[0][1])
raise CanCan::AccessDenied.new(message, actions[0][0], actions[0][1])
end
end
Included an helper for the Controllers:
module CanCanAddition
def multi_authorize!(*args)
#_authorized = true
current_ability.multi_authorize!(*args)
end
end
if defined? ActionController::Base
ActionController::Base.class_eval do
include ApplicationHelper::CanCanAddition
end
end
Which I call like this:
def create
multi_authorize! [:create, Model1], [:authorize, Model2], :message => "You are not authorized to perform this action!"
# other code...
end
WARNING: Due to the code in the ability class, you must provide a message or the last pair of authorization will not be passed in the *args. I'll take some time to overcome this but the idea of the solution I think fits nice with.

Related

Working with conditionals in rails helpers

I have a simple helper in my rails app that sets a css class based on the state of an object, like:
<li class='<%= car_color(car.state) %>'><%= car.model %></li>
in the helper it's basically:
module CarHelper
def car_color(state)
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
end
And it works fine for my usecase. However, how there's a new requirement that a User with a role of customer should not see the color coding that this helper creates.
My first thought was to do a check in the controller to see if the user should be able to see the color coding:
class CarsController < ApplicationController
before_action :is_customer?
# bunch of restful stuff
private
def is_customer?
#customer if current_user.roles.include? 'customer'
end
end
And then in my helper, I can just add a line:
def car_color(color)
return 'car' if #customer
end
This meets the requirements, but it smells to me. Now my helper has a dependency on #customer which is passed simply because it is an instance variable. An alternative would be to explicitly pass in a user.role to car_color or to wrap all of the calls to car_color in conditionals based on the user.role, which seems even worse.
Is there a way to help prepare this code for even more conditionals based on different user roles? My thought is to do something like:
module CarHelper
def car_color(args)
set_color_for_role(args[:user_role]) if args[:user_role]
set_color_for_state(args[:car_state]) if args[:car_state]
end
private
def set_color_for_role(user_role)
# stuff
end
def set_color_for_state(car_state)
# stuff
end
end
I don't use rails helpers very often since I mostly work in angular and am wondering if I'm missing a cleaner OOP approach.
I don't see any issue with checking the current user's roles in the helper method.
You could move the checking behaviour to the user model though which would make things cleaner (you may of course want to generalise this for multiple roles):
class User < ActiveRecord::Base
def is_customer?
self.roles.include?('customer')
end
end
Then in your helper you can just check if current_user.is_customer?
def car_color(state)
if current_user.is_customer?
'car'
else
if state == 'in service'
'car car-in-service'
elsif state == 'in garage'
'car car-in-garage'
else
'car'
end
end
I find it useful sometimes to build up an array of the classes too which is often cleaner (I've thrown in a case too):
def car_color(state)
car_classes = ['car']
unless current_user.is_customer?
car_classes << case state
when 'in service' then 'car-in-service'
when 'in garage' then 'car-in-garage'
end
end
car_classes.join(" ")
end
Use the draper https://github.com/drapergem/draper gem and move this logic to decorator

Why are `scope`-oriented actions (particularly `index` actions) treated differently in Pundit?

I am writing with respect to https://github.com/elabs/pundit#scopes
I am under the impression that authorization should answer the question Are you allowed access to this resource?, i.e. a true/false answer. This is the case with all actions except index, which, according to Pundit's docs, should return different ActiveRecord::Relation's depending on who is asking. For example, an admin gets scope.all, while a regular user gets scope.where(:published => true).
app/policies/post_policy.rb
class Scope < Struct.new(:user, :scope)
def resolve
if user.admin?
scope.all
else
scope.where(:published => true)
end
end
end
app/controllers/posts_controller.rb
def index
#posts = policy_scope(Post)
end
My reservation is that this is a slippery slope, and soon I will be adding presentation to the scopes (e.g. scope.all.order('created_at ASC')) -- and it just feels weird doing so in an authorization policy.
Of course I could move that to the controller...
def index
#post = policy_scope(post)
if user.admin?
#post = #post.order( 'created_at ASC' )
end
end
...but is that the controller's job? And I don't think it would be right to add such a call to the view. So maybe it should be a model method?
What would you say are the pros/cons of doing the following instead?
app/controllers/posts_controller.rb
This keeps index just like the other methods, with one call to authorize, and one call to a model method.
def index
authorize(Post)
#posts = Post.index(current_user)
end
app/policies/post_policy.rb
This simply gives a true/false answer. Are you authorized? Yes or no.
def index?
user.admin? || user.regular_user?
end
app/models/post.rb
And in the model we can get as fancy as we like.
def self.index(user)
if user.admin?
Post.all.order('created_at ASC')
else
Post.where(user_id: user.id)
end
end
Thoughts?
My understanding of authorization vs scopes in Pundit is as follows:
authorization: 'is this user allowed to act upon (create/update/destroy) this resource?'
within scope : 'should this user be able to see (index/show) this resource?'
Authorization (authorize #resource) defers to permitted_attributes in ResourcePolicy for the answer.
Scopes (policy_scope(Resource)) defer to resolve.
I believe the reasoning behind Pundit's scopes is that there should be only one location in your code where you define who should have access to what resources.
You could, as you've described, implement the same behavior in your controllers or your views. However, putting the code into a Policy guards against unauthorized access should you happen to forget to scope appropriately in one of your controller methods.
I think of policy_scope() as the way to restrict visibility, while other result refinements (e.g. sorting) can take place at the controller level. There's no doubt a lot of personal preference at play, however.

Rails Testing controller methods with instance variables set in before filters

Ok, so my main issue is I have implemented Mailboxer into our project to handle messaging and I am trying to write tests for it. However, I keep stumbling over and over again. I have attempted several different stub/mocks but have made no progress.
We have a conversations_controller.rb that relies on before_filters for setting all the instance variables necessary for doing each action. Then in the controller actions, the instance variables are referenced directly to do any sort of action or to return specific data.
Here is an example of our index action which returns all conversations in the "box" that is specified in the before_filter, of the mailbox also specified in another before_filter:
class ConversationsController < ::ApplicationController
before_filter :get_user_mailbox, only: [:index, :new_message, :show_message, :mark_as_read, :mark_as_unread, :create_message, :reply_message, :update, :destroy_message, :untrash]
before_filter :get_box
def index
if #box.eql? "inbox"
#conversations = #mailbox.inbox
elsif #box.eql? "sentbox"
#conversations = #mailbox.sentbox
else
#conversations = #mailbox.trash
end
end
And before filters:
private
def get_user_mailbox
#user = User.where(:user_name => user.user_name.downcase).where(:email => user.email.downcase).first_or_create
#mailbox = #user.mailbox if #user
end
def get_box
if params[:box].blank? or !["inbox","sentbox","trash"].include?params[:box]
params[:box] = 'inbox'
end
#box = params[:box]
end
So I guess I have 2 questions in one. First, how to I get my tests to generate the correct data #mailbox, #user, and #box that is needed for the index action. Next, how do I pass the fake parameter to set #box to different "inbox/sentbox/trash". I have tried controller.index({box: "inbox"}) but always get "wrong arguments 1 for 0" messages.
I have tried the following in various different ways, but always get nil:class errors which means that my instance variables are definitely not being set properly.
describe "GET 'index' returns correct mailbox box" do
before :each do
#user = User.where(:user_name => 'test').where(:email => 'test#test.com').first_or_create
#mailbox = #user.mailbox
end
it "#index returns inbox when box = 'inbox'" do
mock_model User
User.stub_chain(:where, :where).and_return(#user)
controller.index.should == #mailbox.inbox
end
end
Filters and callbacks are hard to test and debug. Avoid them when possible.
In this case I don't think your before_filter is necessary, thus no need to test it. The better home for the methods is model.
Check my refacoring:
class User < ActiveRecord::Base
delegate :inbox, :sentbox, :trash, to: :mailbox
end
class ConversationsController < ::ApplicationController
def index
#conversations = current_user.send get_box
end
private
def get_box
# your code
end
end
That's all. Should be enough.
You can then test regularly.
First of all, read the oficial documentation for rails testing: using data for testing and passing parameters to controlers is explained there.
To generate data for your tests you can:
fill your test database with some mailbox and users using rails fixtures or use something like factory girl
use mock objects to fake data. I personally use the mocha gem but there are others
I tend to use a combination of both, prefering mock objects when possible and falling back to factory girl when mocking needs too much code.

Rails: Verify correct user across multiple controllers

I have several controllers that require a correct user for their edit/update/delete actions. What is the Rails-way to accomplish the following:
Currently, in each controller I have the following code:
class FooController < ApplicationController
before_filter :correct_user, :only => [:edit, :update, :destroy]
# normal controller code
private
def correct_user
#foo = Foo.find params[:id]
redirect_to some_path unless current_user == #foo.user
end
end
I have similar code in 3 controllers. I started to bring it out to a helper like this:
module ApplicationHelper
def correct_user( object, path )
if object.respond_to? :user
redirect_to path unless object.user == current_user
end
end
But I'm wondering if this is a good way to do it. What's the accepted way to solve this?
Thank you
EDIT
The correct user check here is because I want to make sure it's only the author who can make edits/deltes to each of the objects.
To clarify, the objects would be things like Questions and Posts. I don't want to use something like CanCan as it's overkill for something simple like this.
I really like using RyanB's CanCan, which allows you to both restrict access to actions based on the user, and centralize such authorization into basically a single file.
CanCan on GitHub: https://github.com/ryanb/cancan
Screencast explaining how to setup/use it: http://railscasts.com/episodes/192-authorization-with-cancan
EDIT
No problem. I hear you on CanCan - it takes a little while to get up and running on it, but it's designed to do exactly what you're asking - per object authorization.
Alternative:
Another way to do this is move your authoriship/current_user check to the ApplicationController class, from which all of your other Controllers inherit (so they will get that code through inheritance - and you don't need to write the same code in multiple Controllers), and it would look something like...
class ApplicationController < ActionController::Base
...
helper_method :correct_user
private
def correct_user( object, path )
redirect_to path unless object.user == current_user
end
end
You should do the following :
def edit
#foo = current_user.foos.find(params[:id])
end
This way, only if the current user is the owner of the Foo he will be able to see it.

Rails - Flow control question, is there a better way?

I am trying to lock-down a few controllers based on role and the 'posts' controller by whether or not they ANY permissions assigned. This appears to be working, but I'm wondering if there is a clean way to handle this. This is what I have in the application controller, which I'm calling as a before filter...
if controller_name == 'users' || 'accounts'
unless #current_user.master? || #current_user.power?
render :template => "layouts/no_content"
end
elsif controller_name == 'posts'
unless #current_user.permissions.count > 0
render :template => "layouts/no_content"
end
end
Thanks in advance.
You shouldn't make a code snippet that checks for a controller name to take a specific action in application.rb. You should define that before filters only in the controllers that need them
Make 2 methods in ApplicationController:
private
def require_master_or_power_user
unless #current_user.master? || #current_user.power?
render :template => "layouts/no_content"
end
end
def require_some_permisions
unless #current_user.permissions.count > 0
render :template => "layouts/no_content"
end
end
Now add this as a before filter where you need it:
class UsersController < ApplicationController
before_filter :require_master_or_power_user
...
end
class AccountsController < ApplicationController
before_filter :require_master_or_power_user
...
end
class PostsController < ApplicationController
before_filter :require_some_permisions
...
end
So the ApplicationController defines the filters, but its up to your other controllers whether or not to actually use those filters. A superclass like the ApplicationController should never conditionally branch its execution based on its subclasses. Choosing when to use the provided behaviours are one of the reasons why you want to subclass in the first place.
It's also much clearer from a code readability standpoint. When looking at the UsersController, its immediately obvious there is some permission stuff happening when you see a before filter with the name like "require_something". With your approach, you can't tell that from looking at the users controller code itself at all.
I would strongly suggest you adhere to MVC and OOP and move as much of the user related logic back into the User model like this:
class User < ActiveRecord::Base
def has_permission?
true if self.master? || self.power? || (self.permissions.count > 1)
end
then you could just use one filter in application.rb:
protected
def check_template
render :template => "layouts/no_content" if current_user.has_permission? == true
end
and call that with a before_filter as suggested by Squeegy, either in the respective controllers, or site wide in application_controller.rb
before_filter :check_template
This approach is obviously a little cleaner and a lot less brittle if you ever decide to change the scope of what gives people permission, you only have to make one change application wide.
I would advise that you use an ACL system for this: http://github.com/ezmobius/acl_system2
A short little handwritten DSL. Haven't even checked the code for syntax errors, but you'll get the picture. In your application controller:
before_filter :handle_requirements
def self.requirement(*controllers, &block)
#_requirements ||= {}
#_requirements[controllers] = block
end
def handle_requirements
return unless #_requirements
#_requirements.each do |controllers, proc|
if controllers.include?(controller.controller_name)
restrict_access unless instance_eval(&block)
end
end
end
def restrict_access
render :template => "layouts/no_content"
end
Usage (also in your application controller)
requirement('users', 'accounts') do
#current_user.master? || #current_user.power?
end
Or, just use the ACL system Radar mentions.
Another plugin worth a look is role requirement, which I've been using. I think they can both do roughly the same things.
Here is a plug for RESTful_ACL; an ACL plugin/gem I've developed, and is being pretty widely used. It give you freedom to design your roles as you see fit, and it very transparent.

Resources