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

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.

Related

Pundit authorization in index

I have been recently reading through the pundit gem's README and noticed that they never authorize the index view within a controller. (Instead they use scope).
They give good reasoning for this, as an index page generally contains a list of elements, by controlling the list that is generated you effectively control the data on the page. However, occasionally it may be desired to block access to even the index page itself. (Rather than allowing access to a blank index page.) My question is what would be the proper way to perform this?
I have so far come up with several possibilities, and have the following classes:
A model MyModel
A controller MyModelsController
A policy MyModelPolicy
In my index method of my controller, the recommended method to solve this would be as follows:
def index
#my_models = policy_Scope(MyModel)
end
This will then allow access to the index page, but will filter the results to only what that use can see. (E.G. no results for no access.)
However to block access to the index page itself I have arrived at two different possibilities:
def index
#my_models = policy_Scope(MyModel)
authorize #my_models
end
or
def index
#my_models = policy_Scope(MyModel)
authorize MyModel
end
Which of these would be the correct path, or is there a different alternative that would be preferred?
class MyModelPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
raise Pundit::NotAuthorizedError, 'not allowed to view this action'
end
end
end
end
Policy,
class MyModelPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(user: user)
end
end
end
def index?
user.admin?
end
end
Controller,
def index
#my_models = policy_scope(MyModel)
authorize MyModel
end

Should I have a different admin? method for the view and for the before_filter?

I'm learning Rails and working on DinnerDash.
In application_controller.rb I have:
helper_method :admin?
def admin?
current_user.admin_code == 'secret' if current_user
end
So I could use if admin? in my view files to display certain things only to admins. Now I want to write a before_filter that checks if the current_user is an admin and if not, redirects.
It seems to me that I have to write another method to do this. For view files, I want the method to return false if the user isn't an admin, and for the before_filter, I want it to redirect.
Still, something tells me that this isn't the most efficient way to do this. Since I'm learning Rails, I don't want to develop any bad habits of writing code that isn't DRY. Any ideas on how to best handle this situation?
I would make admin? an instance method of the User model. I think it belongs there because you're actually asking for information about a user object.
Then, for the before_filter, I would do something like this:
before_filter :admin_or_redirect
def admin_or_redirect
redirect_to some_url if !current_user.admin?
end
Then you can still call admin? in your views on #user (which you assign current_user to in your controller), and have a different behavior for your before_filter.
EDIT:
You also want to change your admin? method like this:
class User < ActiveRecord::Base
...
def admin?
self.admin_code == 'secret'
end
end

Authorize with multiple action in CanCan

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.

Rails-way - where to put this kind of helper method?

I'm struggling a bit to find the right place for a helper method. The method basicly 'inspects' a User-model object and should return some information about the 'progress' of the user, eg. "You need to add pictures", "Fill out your address" or "Add your e-mail-adress". None of the conditions I'm checking for are required, it's just like a "This is your profile completeness"-functionality as seen on LinkedIn etc.
Each of these 'actions' have a URL, where the user can complete the action, eg. a URL to the page where they can upload a profile photo if that is missing.
Since I need access to my named routes helpers (eg. new_user_image_path) I'm having a hard time figuring out the Rails-way of structuring the code.
I'd like to return an object with a DSL like this:
class UserCompleteness
def initialize(user)
end
def actions
# Returns an array of actions to be completed
end
def percent
# Returns a 'profile completeness' percentage
end
end
And user it with something like: #completeness = user_completeness(current_user)
However, if I'm adding this to my application_helper I don't have access to my named routes helpers. Same goes if I add it to my User-model.
Where should I place this kind of helper method?
This is a similar problem to that of Mailers. They are models, and should not cross the MVC boundaries, but need to generate views. Try this:
class UserCompleteness
include ActionController::UrlWriter
def initialize(user)
end
def actions
# Returns an array of actions to be completed
new_user_image_path(user)
end
def percent
# Returns a 'profile completeness' percentage
end
end
But be aware you are breaking MVC encapsulation, which might make testing more difficult. If you can get away with some methods in the users helper instead of a class that might be better.
From the little i got your question i think you want a method which you can used in Controller as well as Views.
To Accomplish this simple add method in application_controller.rb and named it hepler_method
Example:-
class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
#current_user ||= User.find_by_id(session[:user])
end
end
you can use method current_user in both Controller as well as views

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